Merge pull request #135 from seam345/sean/update-preview-on-sidecar-change
Update JPEGS on sidecar change
This commit is contained in:
commit
8781ab3777
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
ALTER TABLE site_info
|
ALTER TABLE site_info
|
||||||
DROP COLUMN IF EXISTS periodic_scan_interval,
|
DROP COLUMN IF EXISTS periodic_scan_interval,
|
||||||
DrOP COLUMN IF EXISTS concurrent_workers;
|
DROP COLUMN IF EXISTS concurrent_workers;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
ALTER TABLE media
|
||||||
|
DROP COLUMN IF EXISTS side_car_path,
|
||||||
|
DROP COLUMN IF EXISTS side_car_hash;
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
ALTER TABLE media
|
||||||
|
ADD COLUMN IF NOT EXISTS side_car_path varchar(1024) DEFAULT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS side_car_hash varchar(32) DEFAULT NULL;
|
|
@ -21,6 +21,8 @@ type Media struct {
|
||||||
Favorite bool
|
Favorite bool
|
||||||
Type MediaType
|
Type MediaType
|
||||||
VideoMetadataId *int
|
VideoMetadataId *int
|
||||||
|
SideCarPath *string
|
||||||
|
SideCarHash *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Media) ID() int {
|
func (p *Media) ID() int {
|
||||||
|
@ -51,7 +53,7 @@ type MediaURL struct {
|
||||||
func NewMediaFromRow(row *sql.Row) (*Media, error) {
|
func NewMediaFromRow(row *sql.Row) (*Media, error) {
|
||||||
media := Media{}
|
media := Media{}
|
||||||
|
|
||||||
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
|
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId, &media.SideCarPath, &media.SideCarHash); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) {
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var media Media
|
var media Media
|
||||||
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
|
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId, &media.SideCarPath, &media.SideCarHash); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
medias = append(medias, &media)
|
medias = append(medias, &media)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package scanner
|
package scanner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -231,10 +232,14 @@ func getMediaType(path string) (*MediaType, error) {
|
||||||
|
|
||||||
ext := filepath.Ext(path)
|
ext := filepath.Ext(path)
|
||||||
|
|
||||||
fileExtType := fileExtensions[strings.ToLower(ext)]
|
fileExtType, found := fileExtensions[strings.ToLower(ext)]
|
||||||
|
|
||||||
if fileExtType.isSupported() {
|
if found {
|
||||||
return &fileExtType, nil
|
if fileExtType.isSupported() {
|
||||||
|
return &fileExtType, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New(fmt.Sprintf("unsupported file type '%s' (%s)", ext, fileExtType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If extension was not recognized try to read file header
|
// If extension was not recognized try to read file header
|
||||||
|
|
|
@ -96,10 +96,22 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
|
||||||
return false, errors.Wrap(err, "error processing photo highres")
|
return false, errors.Wrap(err, "error processing photo highres")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate high res jpeg
|
|
||||||
var photoDimensions *PhotoDimensions
|
var photoDimensions *PhotoDimensions
|
||||||
var baseImagePath string = photo.Path
|
var baseImagePath string = photo.Path
|
||||||
|
|
||||||
|
mediaType, err := getMediaType(photo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "could determine if media was photo or video")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaType.isRaw() {
|
||||||
|
err = processRawSideCar(tx, imageData, highResURL, thumbURL, photoCachePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate high res jpeg
|
||||||
if highResURL == nil {
|
if highResURL == nil {
|
||||||
|
|
||||||
contentType, err := imageData.ContentType()
|
contentType, err := imageData.ContentType()
|
||||||
|
@ -116,26 +128,10 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
|
||||||
|
|
||||||
baseImagePath = path.Join(*photoCachePath, highres_name)
|
baseImagePath = path.Join(*photoCachePath, highres_name)
|
||||||
|
|
||||||
err = imageData.EncodeHighRes(tx, baseImagePath)
|
err = generateSaveHighResJPEG(tx, photo.MediaID, imageData, highres_name, baseImagePath, -1)
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "creating high-res cached image")
|
|
||||||
}
|
|
||||||
|
|
||||||
photoDimensions, err = GetPhotoDimensions(baseImagePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileStats, err := os.Stat(baseImagePath)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "reading file stats of highres photo")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg", fileStats.Size())
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrapf(err, "could not insert highres media url (%d, %s)", photo.MediaID, photo.Title)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Verify that highres photo still exists in cache
|
// Verify that highres photo still exists in cache
|
||||||
|
@ -182,19 +178,7 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
|
||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
thumbOutputPath := path.Join(*photoCachePath, thumbnail_name)
|
err = generateSaveThumbnailJPEG(tx, photo.MediaID, thumbnail_name, photoCachePath, baseImagePath, -1)
|
||||||
|
|
||||||
thumbSize, err := EncodeThumbnail(baseImagePath, thumbOutputPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "could not create thumbnail cached image")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStats, err := os.Stat(thumbOutputPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "reading file stats of thumbnail photo")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)", photo.MediaID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg", fileStats.Size())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -270,3 +254,108 @@ func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeMed
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateSaveHighResJPEG(tx *sql.Tx, mediaID int, imageData *EncodeMediaData, highres_name string, imagePath string, urlID int) error {
|
||||||
|
|
||||||
|
err := imageData.EncodeHighRes(tx, imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating high-res cached image")
|
||||||
|
}
|
||||||
|
|
||||||
|
photoDimensions, err := GetPhotoDimensions(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStats, err := os.Stat(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "reading file stats of highres photo")
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlID < 0 {
|
||||||
|
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
mediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg", fileStats.Size())
|
||||||
|
} else {
|
||||||
|
_, err = tx.Exec("UPDATE media_url SET width = ?, height = ?, file_size= ? WHERE url_id = ?",
|
||||||
|
photoDimensions.Width, photoDimensions.Height, fileStats.Size(), urlID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not insert highres media url (%d, %s)", mediaID, highres_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSaveThumbnailJPEG(tx *sql.Tx, mediaID int, thumbnail_name string, photoCachePath *string, baseImagePath string, urlID int) error {
|
||||||
|
thumbOutputPath := path.Join(*photoCachePath, thumbnail_name)
|
||||||
|
|
||||||
|
thumbSize, err := EncodeThumbnail(baseImagePath, thumbOutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not create thumbnail cached image")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStats, err := os.Stat(thumbOutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "reading file stats of thumbnail photo")
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlID < 0 {
|
||||||
|
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
mediaID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg", fileStats.Size())
|
||||||
|
} else {
|
||||||
|
_, err = tx.Exec("UPDATE media_url SET width = ?, height = ?, file_size= ? WHERE url_id = ?",
|
||||||
|
thumbSize.Width, thumbSize.Height, fileStats.Size(), urlID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processRawSideCar(tx *sql.Tx, imageData *EncodeMediaData, highResURL *models.MediaURL, thumbURL *models.MediaURL, photoCachePath *string) error {
|
||||||
|
photo := imageData.media
|
||||||
|
sideCarFileHasChanged := false
|
||||||
|
var currentFileHash *string
|
||||||
|
currentSideCarPath := scanForSideCarFile(photo.Path)
|
||||||
|
|
||||||
|
if currentSideCarPath != nil {
|
||||||
|
currentFileHash = hashSideCarFile(currentSideCarPath)
|
||||||
|
if photo.SideCarHash == nil || *photo.SideCarHash != *currentFileHash {
|
||||||
|
sideCarFileHasChanged = true
|
||||||
|
}
|
||||||
|
} else if photo.SideCarPath != nil { // sidecar has been deleted since last scan
|
||||||
|
sideCarFileHasChanged = true
|
||||||
|
}
|
||||||
|
if sideCarFileHasChanged {
|
||||||
|
fmt.Printf("Detected changed sidecar file for %s recreating JPG's to reflect changes\n", photo.Path)
|
||||||
|
|
||||||
|
// update high res image may be cropped so dimentions and file size can change
|
||||||
|
baseImagePath := path.Join(*photoCachePath, highResURL.MediaName) // update base image path for thumbnail
|
||||||
|
tempHighResPath := baseImagePath + ".hold"
|
||||||
|
os.Rename(baseImagePath, tempHighResPath)
|
||||||
|
err := generateSaveHighResJPEG(tx, photo.MediaID, imageData, highResURL.MediaName, baseImagePath, highResURL.UrlID)
|
||||||
|
if err != nil {
|
||||||
|
os.Rename(tempHighResPath, baseImagePath)
|
||||||
|
return errors.Wrap(err, "recreating high-res cached image")
|
||||||
|
}
|
||||||
|
os.Remove(tempHighResPath)
|
||||||
|
|
||||||
|
// update thumbnail image may be cropped so dimentions and file size can change
|
||||||
|
thumbPath := path.Join(*photoCachePath, thumbURL.MediaName)
|
||||||
|
tempThumbPath := thumbPath + ".hold" // hold onto the original image incase for some reason we fail to recreate one with the new settings
|
||||||
|
os.Rename(thumbPath, tempThumbPath)
|
||||||
|
err = generateSaveThumbnailJPEG(tx, photo.MediaID, thumbURL.MediaName, photoCachePath, baseImagePath, thumbURL.UrlID)
|
||||||
|
if err != nil {
|
||||||
|
os.Rename(tempThumbPath, thumbPath)
|
||||||
|
return errors.Wrap(err, "recreating thumbnail cached image")
|
||||||
|
}
|
||||||
|
os.Remove(tempThumbPath)
|
||||||
|
|
||||||
|
// save new side car hash
|
||||||
|
_, err = tx.Exec("UPDATE media SET side_car_hash = ?, side_car_path = ? WHERE media_id = ?", currentFileHash, currentSideCarPath, photo.MediaID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not update side car hash for media: %s", photo.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package scanner
|
package scanner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -10,6 +13,40 @@ import (
|
||||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func scanForSideCarFile(path string) *string {
|
||||||
|
testPath := path + ".xmp"
|
||||||
|
_, err := os.Stat(testPath)
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
// unexpected error logging
|
||||||
|
log.Printf("ERROR: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &testPath
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashSideCarFile(path *string) *string {
|
||||||
|
if path == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(*path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
log.Printf("ERROR: %s", err)
|
||||||
|
}
|
||||||
|
hash := hex.EncodeToString(h.Sum(nil))
|
||||||
|
return &hash
|
||||||
|
}
|
||||||
|
|
||||||
func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCache) (*models.Media, bool, error) {
|
func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCache) (*models.Media, bool, error) {
|
||||||
mediaName := path.Base(mediaPath)
|
mediaName := path.Base(mediaPath)
|
||||||
|
|
||||||
|
@ -35,10 +72,22 @@ func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCac
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaTypeText string
|
var mediaTypeText string
|
||||||
|
|
||||||
|
var sideCarPath *string
|
||||||
|
sideCarPath = nil
|
||||||
|
var sideCarHash *string
|
||||||
|
sideCarHash = nil
|
||||||
if mediaType.isVideo() {
|
if mediaType.isVideo() {
|
||||||
mediaTypeText = "video"
|
mediaTypeText = "video"
|
||||||
} else {
|
} else {
|
||||||
mediaTypeText = "photo"
|
mediaTypeText = "photo"
|
||||||
|
// search for sidecar files
|
||||||
|
if mediaType.isRaw() {
|
||||||
|
sideCarPath = scanForSideCarFile(mediaPath)
|
||||||
|
if sideCarPath != nil {
|
||||||
|
sideCarHash = hashSideCarFile(sideCarPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := os.Stat(mediaPath)
|
stat, err := os.Stat(mediaPath)
|
||||||
|
@ -46,7 +95,7 @@ func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCac
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := tx.Exec("INSERT INTO media (title, path, path_hash, album_id, media_type, date_shot) VALUES (?, ?, MD5(path), ?, ?, ?)", mediaName, mediaPath, albumId, mediaTypeText, stat.ModTime())
|
result, err := tx.Exec("INSERT INTO media (title, path, path_hash, side_car_path, side_car_hash, album_id, media_type, date_shot) VALUES (?, ?, MD5(path), ?, ?, ?, ?, ?)", mediaName, mediaPath, sideCarPath, sideCarHash, albumId, mediaTypeText, stat.ModTime())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, errors.Wrap(err, "could not insert media into database")
|
return nil, false, errors.Wrap(err, "could not insert media into database")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue