mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
Add blurhash task, instead of a global blurhash generating. (#1250)
This commit is contained in:
@@ -2,11 +2,11 @@ package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_tasks"
|
||||
@@ -77,7 +77,7 @@ var ErrorInvalidRootPath = errors.New("invalid root path")
|
||||
func ValidRootPath(rootPath string) bool {
|
||||
_, err := os.Stat(rootPath)
|
||||
if err != nil {
|
||||
log.Printf("Warn: invalid root path: '%s'\n%s\n", rootPath, err)
|
||||
log.Warn(nil, "invalid root path", "root_path", rootPath, "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -124,10 +124,11 @@ func findMediaForAlbum(ctx scanner_task.TaskContext) ([]*models.Media, error) {
|
||||
|
||||
for _, item := range dirContent {
|
||||
mediaPath := path.Join(ctx.GetAlbum().Path, item.Name())
|
||||
log.Info(ctx, "Check the media", "media_path", mediaPath)
|
||||
|
||||
isDirSymlink, err := utils.IsDirSymlink(mediaPath)
|
||||
if err != nil {
|
||||
log.Printf("Cannot detect whether %s is symlink to a directory. Pretending it is not", mediaPath)
|
||||
log.Warn(ctx, "Cannot detect whether the path is symlink to a directory. Pretending it is not", "media_path", mediaPath)
|
||||
isDirSymlink = false
|
||||
}
|
||||
|
||||
|
||||
@@ -172,18 +172,6 @@ func (queue *ScannerQueue) processQueue(notifyThrottle *utils.Throttle) {
|
||||
queue.mutex.Unlock()
|
||||
|
||||
if inProgressLength+upNextLength == 0 {
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: globalScannerProgress,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "Generating blurhashes",
|
||||
Content: "Generating blurhashes for newly scanned media",
|
||||
Positive: true,
|
||||
})
|
||||
|
||||
if err := scanner.GenerateBlurhashes(queue.db); err != nil {
|
||||
scanner_utils.ScannerError(nil, "Failed to generate blurhashes: %v", err)
|
||||
}
|
||||
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: globalScannerProgress,
|
||||
Type: models.NotificationTypeMessage,
|
||||
|
||||
87
api/scanner/scanner_tasks/blurhash_task.go
Normal file
87
api/scanner/scanner_tasks/blurhash_task.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package scanner_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"os"
|
||||
|
||||
"github.com/buckket/go-blurhash"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
)
|
||||
|
||||
type BlurhashTask struct {
|
||||
scanner_task.ScannerTaskBase
|
||||
}
|
||||
|
||||
func (t BlurhashTask) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error {
|
||||
hasThumbnailUpdated := false
|
||||
for _, url := range updatedURLs {
|
||||
if url.Purpose == models.PhotoThumbnail || url.Purpose == models.VideoThumbnail {
|
||||
hasThumbnailUpdated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var media *models.Media
|
||||
if err := ctx.GetDB().Preload("MediaURL").Where("id = ?", mediaData.Media.ID).First(&media).Error; err != nil {
|
||||
return fmt.Errorf("failed to get media(id:%d): %w", mediaData.Media.ID, err)
|
||||
}
|
||||
|
||||
if media.Blurhash != nil && !hasThumbnailUpdated {
|
||||
log.Info(ctx, "No thumbnail updated, ignore generating blurhash", "media", media.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
thumbnail, err := media.GetThumbnail()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get thumbnail of image %q: %w", mediaData.Media.Path, err)
|
||||
}
|
||||
|
||||
hashStr, err := generateBlurhashFromThumbnail(thumbnail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate blurhash of image %q: %w", mediaData.Media.Path, err)
|
||||
}
|
||||
|
||||
media.Blurhash = &hashStr
|
||||
if err := ctx.GetDB().Select("blurhash").Save(media).Error; err != nil {
|
||||
return fmt.Errorf("failed to store blurhash of image %q: %w", mediaData.Media.Path, err)
|
||||
}
|
||||
|
||||
log.Info(ctx, "Generated blurhash of image", "media", mediaData.Media.Path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateBlurhashFromThumbnail generates a blurhash for a single media and stores it in the database
|
||||
func generateBlurhashFromThumbnail(thumbnail *models.MediaURL) (string, error) {
|
||||
path, err := thumbnail.CachedPath()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get path of media(id:%d) error: %w", thumbnail.MediaID, err)
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open %q error: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
imageData, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode %q error: %w", path, err)
|
||||
}
|
||||
|
||||
const (
|
||||
componentX = 4
|
||||
componentY = 3
|
||||
)
|
||||
hashStr, err := blurhash.Encode(componentX, componentY, imageData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode blurhash of %q error: %w", path, err)
|
||||
}
|
||||
|
||||
return hashStr, nil
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/pkg/errors"
|
||||
@@ -24,7 +23,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
updatedURLs := make([]*models.MediaURL, 0)
|
||||
photo := mediaData.Media
|
||||
|
||||
log.Printf("Processing photo: %s\n", photo.Path)
|
||||
log.Info(ctx, "Processing photo", "photo", photo.Path)
|
||||
|
||||
photoURLFromDB := makePhotoURLChecker(ctx.GetDB(), photo.ID)
|
||||
|
||||
@@ -72,7 +71,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
baseImagePath = path.Join(mediaCachePath, highResURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(baseImagePath); os.IsNotExist(err) {
|
||||
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.MediaName)
|
||||
log.Info(ctx, "High-res photo found in database but not in cache, re-encoding photo to cache", "media_name", highResURL.MediaName)
|
||||
updatedURLs = append(updatedURLs, highResURL)
|
||||
|
||||
err = mediaData.EncodeHighRes(baseImagePath)
|
||||
@@ -114,7 +113,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
|
||||
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
|
||||
updatedURLs = append(updatedURLs, thumbURL)
|
||||
fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.MediaName)
|
||||
log.Info(ctx, "Thumbnail photo found in database but not in cache, re-encoding photo to cache", "media_name", thumbURL.MediaName)
|
||||
|
||||
_, err := media_encoding.EncodeThumbnail(ctx.GetDB(), baseImagePath, thumbPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,13 +3,13 @@ package processing_tasks
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
@@ -30,7 +30,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
updatedURLs := make([]*models.MediaURL, 0)
|
||||
video := mediaData.Media
|
||||
|
||||
log.Printf("Processing video: %s", video.Path)
|
||||
log.Info(ctx, "Processing video", "video", video.Path)
|
||||
|
||||
mediaURLFromDB := makePhotoURLChecker(ctx.GetDB(), video.ID)
|
||||
|
||||
@@ -173,7 +173,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
thumbImagePath := path.Join(mediaCachePath, videoThumbnailURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(thumbImagePath); os.IsNotExist(err) {
|
||||
fmt.Printf("Video thumbnail found in database but not in cache, re-encoding photo to cache: %s\n", videoThumbnailURL.MediaName)
|
||||
log.Info(ctx, "Video thumbnail found in database but not in cache, re-encoding video thumbnail to cache", "video", videoThumbnailURL.MediaName)
|
||||
updatedURLs = append(updatedURLs, videoThumbnailURL)
|
||||
|
||||
err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package scanner_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_tasks/cleanup_tasks"
|
||||
@@ -18,6 +20,7 @@ var allTasks []scanner_task.ScannerTask = []scanner_task.ScannerTask{
|
||||
processing_tasks.ProcessPhotoTask{},
|
||||
processing_tasks.ProcessVideoTask{},
|
||||
FaceDetectionTask{},
|
||||
BlurhashTask{},
|
||||
ExifTask{},
|
||||
VideoMetadataTask{},
|
||||
cleanup_tasks.MediaCleanupTask{},
|
||||
@@ -80,6 +83,7 @@ func (t scannerTasks) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileI
|
||||
}
|
||||
|
||||
if skip {
|
||||
log.Info(ctx, "skip the media", "media_path", mediaPath, "by_task", fmt.Sprintf("%T", task))
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"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, 50, 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.GetThumbnail()
|
||||
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: %v", 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 "", fmt.Errorf("get path of media id=%d error: %w", thumbnail.MediaID, err)
|
||||
}
|
||||
|
||||
imageFile, err := os.Open(thumbnail_path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open %s error: %w", thumbnail_path, err)
|
||||
}
|
||||
|
||||
imageData, _, err := image.Decode(imageFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode %q error: %w", thumbnail_path, err)
|
||||
}
|
||||
|
||||
hashStr, err := blurhash.Encode(4, 3, imageData)
|
||||
if err != nil {
|
||||
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 "", fmt.Errorf("update blurhash of media id=%d error: %w", thumbnail.MediaID, err)
|
||||
// }
|
||||
|
||||
return hashStr, nil
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_queue"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,10 +27,6 @@ func RunScannerOnUser(t *testing.T, db *gorm.DB, user *models.User) {
|
||||
|
||||
// wait for all jobs to finish
|
||||
scanner_queue.CloseScannerQueue()
|
||||
|
||||
if err := scanner.GenerateBlurhashes(db); err != nil {
|
||||
t.Fatalf("generate blurhashes error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func RunScannerAll(t *testing.T, db *gorm.DB) {
|
||||
@@ -51,8 +46,4 @@ func RunScannerAll(t *testing.T, db *gorm.DB) {
|
||||
|
||||
// wait for all jobs to finish
|
||||
scanner_queue.CloseScannerQueue()
|
||||
|
||||
if err := scanner.GenerateBlurhashes(db); err != nil {
|
||||
t.Fatalf("generate blurhashes error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user