Improve scanner - first scan, then process
This commit is contained in:
parent
440814564c
commit
458b6fb49c
|
@ -68,7 +68,7 @@ func DeregisterListener(listenerID int) error {
|
|||
|
||||
func BroadcastNotification(notification *models.Notification) {
|
||||
|
||||
log.Println("Broadcasting notification")
|
||||
log.Printf("Broadcasting notification: %s\n", notification.Header)
|
||||
|
||||
notificationLock.Lock()
|
||||
defer notificationLock.Unlock()
|
||||
|
|
|
@ -143,6 +143,28 @@ func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*mode
|
|||
return url, nil
|
||||
}
|
||||
|
||||
// func processPhoto(db *sql.DB, photo *models.Photo) error {
|
||||
// tx, err := db.Begin()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// err = scanner.ProcessPhoto(tx, photo)
|
||||
// if err != nil {
|
||||
// tx.Rollback()
|
||||
// log.Printf("ERROR: Could not process photo: %s\n", err)
|
||||
// return errors.New(fmt.Sprintf("Could not process photo: %s\n", err))
|
||||
// }
|
||||
|
||||
// err = tx.Commit()
|
||||
// if err != nil {
|
||||
// log.Printf("ERROR: Could not commit photo after process to db: %s\n", err)
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
|
|
@ -133,7 +133,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
|||
return
|
||||
}
|
||||
|
||||
err = scanner.ProcessPhoto(tx, photo, &content_type)
|
||||
err = scanner.ProcessPhoto(tx, photo)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: processing image not found in cache: %s\n", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
|
|
@ -81,11 +81,13 @@ func scan(database *sql.DB, user *models.User) {
|
|||
notifyKey := utils.GenerateToken()
|
||||
processKey := utils.GenerateToken()
|
||||
|
||||
timeout := 3000
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: notifyKey,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "User scan started",
|
||||
Content: "Scanning has started...",
|
||||
Timeout: &timeout,
|
||||
})
|
||||
|
||||
// Start scanning
|
||||
|
@ -103,6 +105,8 @@ func scan(database *sql.DB, user *models.User) {
|
|||
parentId: nil,
|
||||
})
|
||||
|
||||
photosToProcess := list.New()
|
||||
|
||||
for scanQueue.Front() != nil {
|
||||
albumInfo := scanQueue.Front().Value.(scanInfo)
|
||||
scanQueue.Remove(scanQueue.Front())
|
||||
|
@ -161,16 +165,24 @@ func scan(database *sql.DB, user *models.User) {
|
|||
continue
|
||||
}
|
||||
|
||||
content_type := scanner_cache.get_photo_type(photoPath)
|
||||
if content_type == nil {
|
||||
ScannerError("Content type not found from cache\n")
|
||||
photo, newPhoto, err := ScanPhoto(tx, photoPath, albumId, processKey)
|
||||
if err != nil {
|
||||
ScannerError("Scanning image %s: %s", photoPath, err)
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ScanPhoto(tx, photoPath, albumId, content_type, processKey); err != nil {
|
||||
ScannerError("processing image %s: %s", photoPath, err)
|
||||
tx.Rollback()
|
||||
continue
|
||||
if newPhoto {
|
||||
photosToProcess.PushBack(photo)
|
||||
|
||||
if photosToProcess.Len()%25 == 0 {
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: processKey,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "Scanning photo",
|
||||
Content: fmt.Sprintf("Scanning image at %s", photoPath),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
@ -192,18 +204,71 @@ func scan(database *sql.DB, user *models.User) {
|
|||
|
||||
cleanupCache(database, album_paths_scanned, user)
|
||||
|
||||
completeMessage := "No new photos were found"
|
||||
if photosToProcess.Len() > 0 {
|
||||
completeMessage = fmt.Sprintf("Starting to process %d newly scanned photos", photosToProcess.Len())
|
||||
}
|
||||
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: notifyKey,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "User scan completed",
|
||||
Content: "Scanning has been completed...",
|
||||
Header: "Scan completed",
|
||||
Content: completeMessage,
|
||||
Positive: true,
|
||||
})
|
||||
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: processKey,
|
||||
Type: models.NotificationTypeClose,
|
||||
})
|
||||
// Proccess all photos
|
||||
photosToProcessElm := photosToProcess.Front()
|
||||
processCount := -1
|
||||
for photosToProcessElm != nil {
|
||||
photo := photosToProcessElm.Value.(*models.Photo)
|
||||
photosToProcessElm = photosToProcessElm.Next()
|
||||
processCount++
|
||||
|
||||
tx, err := database.Begin()
|
||||
if err != nil {
|
||||
ScannerError("Could not start database transaction: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var progress float64 = float64(processCount) / float64(photosToProcess.Len()) * 100.0
|
||||
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: processKey,
|
||||
Type: models.NotificationTypeProgress,
|
||||
Header: fmt.Sprintf("Processing photos (%d of %d)", processCount, photosToProcess.Len()),
|
||||
Content: fmt.Sprintf("Processing photo at %s", photo.Path),
|
||||
Progress: &progress,
|
||||
})
|
||||
|
||||
err = ProcessPhoto(tx, photo)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
ScannerError("Could not process photo: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
ScannerError("Could not commit db transaction: %s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if photosToProcess.Len() > 0 {
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: notifyKey,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "Processing completed",
|
||||
Content: fmt.Sprintf("%d photos have been processed", photosToProcess.Len()),
|
||||
Positive: true,
|
||||
})
|
||||
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: processKey,
|
||||
Type: models.NotificationTypeClose,
|
||||
})
|
||||
}
|
||||
|
||||
log.Println("Done scanning")
|
||||
}
|
||||
|
@ -255,7 +320,8 @@ func directoryContainsPhotos(rootPath string, cache *scanner_cache) bool {
|
|||
var SupportedMimetypes = [...]string{
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
// todo: add support for tiff
|
||||
// "image/tiff",
|
||||
"image/webp",
|
||||
"image/x-canon-cr2",
|
||||
"image/bmp",
|
||||
|
@ -321,10 +387,13 @@ func cleanupCache(database *sql.DB, scanned_albums []interface{}, user *models.U
|
|||
deleted_ids := make([]interface{}, 0)
|
||||
for rows.Next() {
|
||||
var album_id int
|
||||
rows.Scan(&album_id)
|
||||
if err := rows.Scan(&album_id); err != nil {
|
||||
ScannerError("Could not parse album to be removed (album_id %d): %s\n", album_id, err)
|
||||
}
|
||||
|
||||
deleted_ids = append(deleted_ids, album_id)
|
||||
deleted_albums++
|
||||
cache_path := path.Join("./image-cache", strconv.Itoa(album_id))
|
||||
cache_path := path.Join("./photo_cache", strconv.Itoa(album_id))
|
||||
err := os.RemoveAll(cache_path)
|
||||
if err != nil {
|
||||
ScannerError("Could not delete unused cache folder: %s\n%s\n", cache_path, err)
|
||||
|
|
|
@ -99,22 +99,25 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (*models.PhotoEXIF, error) {
|
|||
}
|
||||
}
|
||||
|
||||
focalLengthRat, err := readRationalTag(exifTags, exif.FocalLength, photo)
|
||||
focalLengthTag, err := exifTags.Get(exif.FocalLength)
|
||||
if err == nil {
|
||||
focalLength, _ := focalLengthRat.Float32()
|
||||
valueNames = append(valueNames, "focal_length")
|
||||
exifValues = append(exifValues, focalLength)
|
||||
} else {
|
||||
// For some photos, the focal length cannot be read as a rational value,
|
||||
// but is instead the second value read as an integer
|
||||
tag, err := exifTags.Get(exif.FocalLength)
|
||||
focalLengthRat, err := focalLengthTag.Rat(0)
|
||||
if err == nil {
|
||||
focalLength, err := tag.Int(1)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse EXIF FocalLength as integer: %s\n%s\n", photo.Title, err)
|
||||
} else {
|
||||
valueNames = append(valueNames, "focal_length")
|
||||
exifValues = append(exifValues, focalLength)
|
||||
focalLength, _ := focalLengthRat.Float32()
|
||||
valueNames = append(valueNames, "focal_length")
|
||||
exifValues = append(exifValues, focalLength)
|
||||
} else {
|
||||
// For some photos, the focal length cannot be read as a rational value,
|
||||
// but is instead the second value read as an integer
|
||||
|
||||
if err == nil {
|
||||
focalLength, err := focalLengthTag.Int(1)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %s\n%s\n", photo.Title, err)
|
||||
} else {
|
||||
valueNames = append(valueNames, "focal_length")
|
||||
exifValues = append(exifValues, focalLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,55 +2,46 @@ package scanner
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/graphql/notification"
|
||||
)
|
||||
|
||||
func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string, notificationKey string) error {
|
||||
func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, notificationKey string) (*models.Photo, bool, error) {
|
||||
|
||||
log.Printf("Scanning image: %s\n", photoPath)
|
||||
|
||||
photoName := path.Base(photoPath)
|
||||
|
||||
// Check if image already exists
|
||||
row := tx.QueryRow("SELECT (photo_id) FROM photo WHERE path = ?", photoPath)
|
||||
var photo_id int64
|
||||
if err := row.Scan(&photo_id); err != sql.ErrNoRows {
|
||||
if err == nil {
|
||||
log.Printf("Image already scanned: %s\n", photoPath)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
{
|
||||
row := tx.QueryRow("SELECT * FROM photo WHERE path = ?", photoPath)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
if err != sql.ErrNoRows {
|
||||
if err == nil {
|
||||
log.Printf("Image already scanned: %s\n", photoPath)
|
||||
return photo, false, nil
|
||||
} else {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyTimeout := 5000
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
Key: notificationKey,
|
||||
Type: models.NotificationTypeMessage,
|
||||
Header: "Scanning photo",
|
||||
Content: fmt.Sprintf("Scanning image at %s", photoPath),
|
||||
Timeout: ¬ifyTimeout,
|
||||
})
|
||||
|
||||
result, err := tx.Exec("INSERT INTO photo (title, path, album_id) VALUES (?, ?, ?)", photoName, photoPath, albumId)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not insert photo into database")
|
||||
return err
|
||||
return nil, false, err
|
||||
}
|
||||
photo_id, err = result.LastInsertId()
|
||||
photo_id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
row = tx.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
|
||||
row := tx.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
_, err = ScanEXIF(tx, photo)
|
||||
|
@ -58,9 +49,5 @@ func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string,
|
|||
log.Printf("ERROR: ScanEXIF for %s: %s\n", photoName, err)
|
||||
}
|
||||
|
||||
if err := ProcessPhoto(tx, photo, content_type); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return photo, true, nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/h2non/filetype"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// Higher order function used to check if PhotoURL for a given PhotoPurpose exists
|
||||
func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.PhotoPurpose) (*models.PhotoURL, error), error) {
|
||||
photoURLExistsStmt, err := tx.Prepare("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?")
|
||||
if err != nil {
|
||||
|
@ -45,7 +47,7 @@ func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.PhotoPurp
|
|||
}, nil
|
||||
}
|
||||
|
||||
func ProcessPhoto(tx *sql.Tx, photo *models.Photo, content_type *string) error {
|
||||
func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
||||
|
||||
log.Printf("Processing photo: %s\n", photo.Path)
|
||||
|
||||
|
@ -78,7 +80,12 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo, content_type *string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoOriginal, content_type)
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoOriginal, contentType)
|
||||
if err != nil {
|
||||
log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
|
||||
return err
|
||||
|
@ -145,17 +152,22 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo, content_type *string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// high res
|
||||
original_web_safe := false
|
||||
for _, web_mime := range WebMimetypes {
|
||||
if *content_type == web_mime {
|
||||
original_web_safe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Generate high res jpeg
|
||||
if highResURL == nil {
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
original_web_safe := false
|
||||
for _, web_mime := range WebMimetypes {
|
||||
if *contentType == web_mime {
|
||||
original_web_safe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !original_web_safe {
|
||||
highres_name := fmt.Sprintf("highres_%s_%s", photoName, utils.GenerateToken())
|
||||
highres_name = strings.ReplaceAll(highres_name, ".", "_")
|
||||
|
@ -253,6 +265,34 @@ type ProcessImageData struct {
|
|||
photoPath string
|
||||
_photoImage image.Image
|
||||
_thumbnailImage image.Image
|
||||
_contentType *string
|
||||
}
|
||||
|
||||
func (img *ProcessImageData) ContentType() (*string, error) {
|
||||
if img._contentType != nil {
|
||||
return img._contentType, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(img.photoPath)
|
||||
if err != nil {
|
||||
ScannerError("Could not open file %s: %s\n", img.photoPath, err)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 261)
|
||||
if _, err := file.Read(head); err != nil {
|
||||
ScannerError("Could not read file %s: %s\n", img.photoPath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imgType, err := filetype.Image(head)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img._contentType = &imgType.MIME.Value
|
||||
return img._contentType, nil
|
||||
}
|
||||
|
||||
func (img *ProcessImageData) PhotoImage() (image.Image, error) {
|
||||
|
|
|
@ -63,6 +63,7 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
|
|||
content: msg.content,
|
||||
negative: msg.negative,
|
||||
positive: msg.positive,
|
||||
percent: msg.progress,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue