Photo duplication detection (#148)
* Fixes viktorstrate/photoview#8 - Added new property CounterpartPath to Media struct to hold the path to the counterpart JPEG file (if any) - Added new MediaType method isBasicSupportedisBasicTypeSupported() - Added new function isFileExists() to minimize the code duplication * Fixes viktorstrate/photoview#8 - Chaned CounterpartPath definition from string to *string - Added new helper method FileExtensions() - Simplified the logic inside scanForRawCounterpartFile() and scanForCompressedCounterpartFile() functions, reducing the code duplication * Fixes viktorstrate/photoview#8 - Added debug to fileExists() function * Cleanup fileExists logging Co-authored-by: viktorstrate <viktorstrate@gmail.com>
This commit is contained in:
parent
1fbdaf101f
commit
6adc79001c
|
@ -23,6 +23,7 @@ type Media struct {
|
||||||
VideoMetadataId *int
|
VideoMetadataId *int
|
||||||
SideCarPath *string
|
SideCarPath *string
|
||||||
SideCarHash *string
|
SideCarHash *string
|
||||||
|
CounterpartPath *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Media) ID() int {
|
func (p *Media) ID() int {
|
||||||
|
|
|
@ -126,7 +126,8 @@ func (img *EncodeMediaData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
|
||||||
return errors.New("could not convert photo as file format is not supported")
|
return errors.New("could not convert photo as file format is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentType.isRaw() {
|
// Use darktable if there is no counterpart JPEG file to use instead
|
||||||
|
if contentType.isRaw() && img.media.CounterpartPath == nil {
|
||||||
if DarktableCli.IsInstalled() {
|
if DarktableCli.IsInstalled() {
|
||||||
err := DarktableCli.EncodeJpeg(img.media.Path, outputPath, 70)
|
err := DarktableCli.EncodeJpeg(img.media.Path, outputPath, 70)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -170,21 +171,16 @@ func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
|
||||||
return img._photoImage, nil
|
return img._photoImage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
photoImg, err := DecodeImage(img.media.Path)
|
var photoPath string
|
||||||
if err != nil {
|
if img.media.CounterpartPath != nil {
|
||||||
return nil, utils.HandleError("image decoding", err)
|
photoPath = *img.media.CounterpartPath
|
||||||
|
} else {
|
||||||
|
photoPath = img.media.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get orientation from exif data
|
photoImg, err := DecodeImage(photoPath)
|
||||||
row := tx.QueryRow("SELECT media_exif.orientation FROM media JOIN media_exif WHERE media.exif_id = media_exif.exif_id AND media.media_id = ?", img.media.MediaID)
|
if err != nil {
|
||||||
var orientation *int
|
return nil, utils.HandleError("image decoding", err)
|
||||||
if err = row.Scan(&orientation); err != nil {
|
|
||||||
// If not found use default orientation (not rotate)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
orientation = nil
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img._photoImage = photoImg
|
img._photoImage = photoImg
|
||||||
|
|
|
@ -209,14 +209,22 @@ func (imgType *MediaType) isVideo() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSupported determines if the given type can be processed
|
func (imgType *MediaType) isBasicTypeSupported() bool {
|
||||||
func (imgType *MediaType) isSupported() bool {
|
for _, img_mime := range SupportedMimetypes {
|
||||||
for _, supported_mime := range SupportedMimetypes {
|
if img_mime == *imgType {
|
||||||
if supported_mime == *imgType {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSupported determines if the given type can be processed
|
||||||
|
func (imgType *MediaType) isSupported() bool {
|
||||||
|
if imgType.isBasicTypeSupported() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if DarktableCli.IsInstalled() && imgType.isRaw() {
|
if DarktableCli.IsInstalled() && imgType.isRaw() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -296,3 +304,16 @@ func isPathMedia(mediaPath string, cache *AlbumScannerCache) bool {
|
||||||
log.Printf("File is not a supported media %s\n", mediaPath)
|
log.Printf("File is not a supported media %s\n", mediaPath)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mediaType MediaType) FileExtensions() []string {
|
||||||
|
var extensions []string
|
||||||
|
|
||||||
|
for ext, extType := range fileExtensions {
|
||||||
|
if extType == mediaType {
|
||||||
|
extensions = append(extensions, ext)
|
||||||
|
extensions = append(extensions, strings.ToUpper(ext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
}
|
||||||
|
|
|
@ -109,6 +109,11 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
counterpartFile := scanForCompressedCounterpartFile(photo.Path)
|
||||||
|
if counterpartFile != nil {
|
||||||
|
photo.CounterpartPath = counterpartFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate high res jpeg
|
// Generate high res jpeg
|
||||||
|
@ -173,11 +178,6 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
|
||||||
thumbnail_name = models.SanitizeMediaName(thumbnail_name)
|
thumbnail_name = models.SanitizeMediaName(thumbnail_name)
|
||||||
thumbnail_name = thumbnail_name + ".jpg"
|
thumbnail_name = thumbnail_name + ".jpg"
|
||||||
|
|
||||||
// thumbnailImage, err := imageData.ThumbnailImage(tx)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
err = generateSaveThumbnailJPEG(tx, photo.MediaID, thumbnail_name, photoCachePath, baseImagePath, -1)
|
err = generateSaveThumbnailJPEG(tx, photo.MediaID, thumbnail_name, photoCachePath, baseImagePath, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
|
@ -98,6 +98,12 @@ func findMediaForAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB
|
||||||
photoPath := path.Join(album.Path, item.Name())
|
photoPath := path.Join(album.Path, item.Name())
|
||||||
|
|
||||||
if !item.IsDir() && isPathMedia(photoPath, cache) {
|
if !item.IsDir() && isPathMedia(photoPath, cache) {
|
||||||
|
// Skip the JPEGs that are compressed version of raw files
|
||||||
|
counterpartFile := scanForRawCounterpartFile(photoPath)
|
||||||
|
if counterpartFile != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ScannerError("Could not begin database transaction for image %s: %s\n", photoPath, err)
|
ScannerError("Could not begin database transaction for image %s: %s\n", photoPath, err)
|
||||||
|
|
|
@ -8,24 +8,79 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func scanForSideCarFile(path string) *string {
|
func fileExists(testPath string) bool {
|
||||||
testPath := path + ".xmp"
|
|
||||||
_, err := os.Stat(testPath)
|
_, err := os.Stat(testPath)
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil
|
return false
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// unexpected error logging
|
// unexpected error logging
|
||||||
log.Printf("ERROR: %s", err)
|
log.Printf("Error: checking for file existence (%s): %s", testPath, err)
|
||||||
return nil
|
return false
|
||||||
}
|
}
|
||||||
return &testPath
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanForSideCarFile(path string) *string {
|
||||||
|
testPath := path + ".xmp"
|
||||||
|
|
||||||
|
if fileExists(testPath) {
|
||||||
|
return &testPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanForRawCounterpartFile(imagePath string) *string {
|
||||||
|
ext := filepath.Ext(imagePath)
|
||||||
|
fileExtType, found := fileExtensions[strings.ToLower(ext)]
|
||||||
|
|
||||||
|
if found {
|
||||||
|
if !fileExtType.isBasicTypeSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
||||||
|
|
||||||
|
for _, rawType := range RawMimeTypes {
|
||||||
|
for _, ext := range rawType.FileExtensions() {
|
||||||
|
testPath := pathWithoutExt + ext
|
||||||
|
if fileExists(testPath) {
|
||||||
|
return &testPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanForCompressedCounterpartFile(imagePath string) *string {
|
||||||
|
ext := filepath.Ext(imagePath)
|
||||||
|
fileExtType, found := fileExtensions[strings.ToLower(ext)]
|
||||||
|
|
||||||
|
if found {
|
||||||
|
if fileExtType.isBasicTypeSupported() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
||||||
|
for _, ext := range TypeJpeg.FileExtensions() {
|
||||||
|
testPath := pathWithoutExt + ext
|
||||||
|
if fileExists(testPath) {
|
||||||
|
return &testPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashSideCarFile(path *string) *string {
|
func hashSideCarFile(path *string) *string {
|
||||||
|
|
Loading…
Reference in New Issue