Implemented working importer and added unit tests

This commit is contained in:
Michael Mayer 2018-02-27 19:04:48 +01:00
parent c8b7dbbe01
commit 1a465b5002
15 changed files with 694 additions and 355 deletions

88
Gopkg.lock generated
View file

@ -14,33 +14,9 @@
revision = "bbdc45bcf55de61b38b4108871199a117aecd1be"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
[[projects]]
name = "github.com/ddliu/go-httpclient"
packages = ["."]
revision = "52a7afc73c57c5b898b5514a5467f8d38decd3ed"
version = "v0.5.1"
[[projects]]
branch = "master"
name = "github.com/dghubble/go-twitter"
packages = ["twitter"]
revision = "c4115fa44a928413e0b857e0eb47376ffde3a61a"
[[projects]]
name = "github.com/dghubble/oauth1"
packages = ["."]
revision = "70562a5920ad9b6ff03ef697c0f90ae569abbd2b"
version = "v0.4.0"
[[projects]]
name = "github.com/dghubble/sling"
packages = ["."]
revision = "eb56e89ac5088bebb12eef3cb4b293300f43608b"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
@ -55,12 +31,6 @@
revision = "95292e44976d1217cf3611dc7c8d9466877d3ed5"
version = "v1.0.1"
[[projects]]
branch = "master"
name = "github.com/google/go-querystring"
packages = ["query"]
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
name = "github.com/julienschmidt/httprouter"
packages = ["."]
@ -73,30 +43,18 @@
packages = ["yaml"]
revision = "08cad365cd28a7fba23bb1e57aa43c5e18ad8bb8"
[[projects]]
name = "github.com/lastzero/tweethog"
packages = ["."]
revision = "ac3ce5feaebcb1320109e02d5e477d0a97711793"
version = "v0.7.0"
[[projects]]
branch = "master"
name = "github.com/mailru/easyjson"
packages = [".","buffer","jlexer","jwriter"]
revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1"
[[projects]]
name = "github.com/olivere/elastic"
packages = ["config","uritemplates"]
revision = "c51e74f9bcab8906a2f6cf5660dac396ba51b3d6"
version = "v6.1.4"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "go1"
name = "github.com/rwcarlsen/goexif"
@ -110,10 +68,10 @@
version = "0.2.3"
[[projects]]
branch = "master"
name = "github.com/subosito/shorturl"
packages = [".","adfly","base","bitly","catchy","cligs","gggg","gitio","googl","isgd","moourl","pendekin","shorl","snipurl","tinyurl","vamu"]
revision = "3dc4cee684914f665399d6a5cddc13b1864b36dd"
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
name = "github.com/tensorflow/tensorflow"
@ -133,27 +91,9 @@
packages = ["bmp","riff","tiff","tiff/lzw","vp8","vp8l","webp"]
revision = "12117c17ca67ffa1ce22e9409f3b0b0a93ac08c7"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","html","html/atom"]
revision = "309822c5b9b9f80db67f016069a12628d94fad34"
[[projects]]
name = "gopkg.in/olivere/elastic.v6"
packages = ["."]
revision = "c51e74f9bcab8906a2f6cf5660dac396ba51b3d6"
version = "v6.1.4"
[[projects]]
name = "mvdan.cc/xurls"
packages = ["."]
revision = "d315b61cf6727664f310fa87b3197e9faf2a8513"
version = "v1.1.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "70fdd0684ae209f6735c04d5ecffa4c3ad3dc102214afc431b79c530fb2d1d65"
inputs-digest = "ea6460db0c53d350b75d5795872ab17c55d0348840b77a27618da6bd75d329ad"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -5,8 +5,6 @@ import (
"github.com/urfave/cli"
"os"
"fmt"
"gopkg.in/olivere/elastic.v6"
"github.com/lastzero/tweethog"
)
func main() {
@ -23,7 +21,7 @@ func main() {
Name: "config",
Usage: "Displays global configuration values",
Action: func(c *cli.Context) error {
config.SetValuesFromFile(tweethog.GetExpandedFilename(c.GlobalString("config-file")))
config.SetValuesFromFile(photoprism.getExpandedFilename(c.GlobalString("config-file")))
config.SetValuesFromCliContext(c)
@ -49,21 +47,17 @@ func main() {
},
},
Action: func(c *cli.Context) error {
config.SetValuesFromFile(tweethog.GetExpandedFilename(c.GlobalString("config-file")))
config.SetValuesFromFile(photoprism.getExpandedFilename(c.GlobalString("config-file")))
config.SetValuesFromCliContext(c)
fmt.Println("Welcome to PhotoPrism")
fmt.Printf("Importing photos from %s\n", config.ImportPath)
client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
importer := photoprism.NewImporter(config.OriginalsPath)
if err != nil {
fmt.Println("Problem with elasticsearch :-(")
importer.ImportPhotosFromDirectory(config.ImportPath)
return err
}
client.ClusterState()
fmt.Println("Done.")
return nil
},

View file

@ -18,57 +18,57 @@ func NewConfig() *Config {
return &Config{}
}
func (config *Config) SetValuesFromFile(fileName string) error {
func (c *Config) SetValuesFromFile(fileName string) error {
yamlConfig, err := yaml.ReadFile(fileName)
if err != nil {
return err
}
config.ConfigFile = fileName
c.ConfigFile = fileName
if OriginalsPath, err := yamlConfig.Get("originals-path"); err == nil {
config.OriginalsPath = GetExpandedFilename(OriginalsPath)
c.OriginalsPath = getExpandedFilename(OriginalsPath)
}
if ThumbnailsPath, err := yamlConfig.Get("thumbnails-path"); err == nil {
config.ThumbnailsPath = GetExpandedFilename(ThumbnailsPath)
c.ThumbnailsPath = getExpandedFilename(ThumbnailsPath)
}
if ImportPath, err := yamlConfig.Get("import-path"); err == nil {
config.ImportPath = GetExpandedFilename(ImportPath)
c.ImportPath = getExpandedFilename(ImportPath)
}
if ExportPath, err := yamlConfig.Get("export-path"); err == nil {
config.ExportPath = GetExpandedFilename(ExportPath)
c.ExportPath = getExpandedFilename(ExportPath)
}
if DarktableCli, err := yamlConfig.Get("darktable-cli"); err == nil {
config.DarktableCli = GetExpandedFilename(DarktableCli)
c.DarktableCli = getExpandedFilename(DarktableCli)
}
return nil
}
func (config *Config) SetValuesFromCliContext(c *cli.Context) error {
if c.IsSet("originals-path") {
config.OriginalsPath = GetExpandedFilename(c.String("originals-path"))
func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
if context.IsSet("originals-path") {
c.OriginalsPath = getExpandedFilename(context.String("originals-path"))
}
if c.IsSet("thumbnails-path") {
config.ThumbnailsPath = GetExpandedFilename(c.String("thumbnails-path"))
if context.IsSet("thumbnails-path") {
c.ThumbnailsPath = getExpandedFilename(context.String("thumbnails-path"))
}
if c.IsSet("import-path") {
config.ImportPath = GetExpandedFilename(c.String("import-path"))
if context.IsSet("import-path") {
c.ImportPath = getExpandedFilename(context.String("import-path"))
}
if c.IsSet("export-path") {
config.ExportPath = GetExpandedFilename(c.String("export-path"))
if context.IsSet("export-path") {
c.ExportPath = getExpandedFilename(context.String("export-path"))
}
if c.IsSet("darktable-cli") {
config.DarktableCli = GetExpandedFilename(c.String("darktable-cli"))
if context.IsSet("darktable-cli") {
c.DarktableCli = getExpandedFilename(context.String("darktable-cli"))
}
return nil

View file

@ -1,11 +1,88 @@
package photoprism
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"fmt"
)
const testDataPath = "testdata"
const testDataUrl = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1"
const testDataHash = "ed3bdb2fe86ea662bc863b63e219b47b8d9a74024757007f7979887d"
var darktableCli = "/Applications/darktable.app/Contents/MacOS/darktable-cli"
var testDataZip = getExpandedFilename(testDataPath + "/import.zip")
var originalsPath = getExpandedFilename(testDataPath + "/originals")
var thumbnailsPath = getExpandedFilename(testDataPath + "/thumbnails")
var importPath = getExpandedFilename(testDataPath + "/import")
var exportPath = getExpandedFilename(testDataPath + "/export")
func (c *Config) RemoveTestData(t *testing.T) {
os.RemoveAll(c.ImportPath)
os.RemoveAll(c.ExportPath)
os.RemoveAll(c.OriginalsPath)
os.RemoveAll(c.ThumbnailsPath)
}
func (c *Config) DownloadTestData(t *testing.T) {
if fileExists(testDataZip) {
hash := fileHash(testDataZip)
if hash != testDataHash {
os.Remove(testDataZip)
t.Logf("Removed outdated test data zip file (fingerprint %s)\n", hash)
}
}
if !fileExists(testDataZip) {
fmt.Printf("Downloading latest test data zip file from %s\n", testDataUrl)
if err := downloadFile(testDataZip, testDataUrl); err != nil {
fmt.Printf("Download failed: %s\n", err.Error())
}
}
}
func (c *Config) UnzipTestData(t *testing.T) {
if _, err := unzip(testDataZip, testDataPath); err != nil {
t.Logf("Could not unzip test data: %s\n", err.Error())
}
}
func (c *Config) InitializeTestData(t *testing.T) {
t.Log("Initializing test data")
c.RemoveTestData(t)
c.DownloadTestData(t)
c.UnzipTestData(t)
}
func NewTestConfig() *Config {
return &Config{
DarktableCli: "/Applications/darktable.app/Contents/MacOS/darktable-cli",
OriginalsPath: GetExpandedFilename("photos/originals"),
ThumbnailsPath: GetExpandedFilename("photos/thumbnails"),
ImportPath: GetExpandedFilename("photos/import"),
ExportPath: GetExpandedFilename("photos/export"),
DarktableCli: darktableCli,
OriginalsPath: originalsPath,
ThumbnailsPath: thumbnailsPath,
ImportPath: importPath,
ExportPath: exportPath,
}
}
func TestNewConfig(t *testing.T) {
c := NewConfig()
assert.IsType(t, &Config{}, c)
}
func TestConfig_SetValuesFromFile(t *testing.T) {
c := NewConfig()
c.SetValuesFromFile(getExpandedFilename("config.example.yml"))
assert.Equal(t, getExpandedFilename("photos/originals"), c.OriginalsPath)
assert.Equal(t, getExpandedFilename("photos/thumbnails"), c.ThumbnailsPath)
assert.Equal(t, getExpandedFilename("photos/import"), c.ImportPath)
assert.Equal(t, getExpandedFilename("photos/export"), c.ExportPath)
}

View file

@ -4,6 +4,8 @@ import (
"os"
"os/exec"
"log"
"path/filepath"
"errors"
)
type Converter struct {
@ -20,7 +22,44 @@ func NewConverter(darktableCli string) *Converter {
return &Converter{darktableCli: darktableCli}
}
func (converter *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error) {
func (c *Converter) ConvertAll(path string) {
err := filepath.Walk(path, func(filename string, fileInfo os.FileInfo, err error) error {
if err != nil {
log.Print(err.Error())
return nil
}
if fileInfo.IsDir() {
return nil
}
mediaFile := NewMediaFile(filename)
if !mediaFile.Exists() || !mediaFile.IsRaw() {
return nil
}
log.Printf("Converting %s \n", filename)
if _, err := c.ConvertToJpeg(mediaFile); err != nil {
log.Print(err.Error())
}
return nil
})
if err != nil {
log.Print(err.Error())
}
}
func (c *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error) {
if !image.Exists() {
return nil, errors.New("can not convert, file does not exist")
}
if image.IsJpeg() {
return image, nil
}
@ -40,9 +79,9 @@ func (converter *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error)
var convertCommand *exec.Cmd
if _, err := os.Stat(xmpFilename); err == nil {
convertCommand = exec.Command(converter.darktableCli, image.filename, xmpFilename, jpegFilename)
convertCommand = exec.Command(c.darktableCli, image.filename, xmpFilename, jpegFilename)
} else {
convertCommand = exec.Command(converter.darktableCli, image.filename, jpegFilename)
convertCommand = exec.Command(c.darktableCli, image.filename, jpegFilename)
}
if err := convertCommand.Run(); err != nil {

View file

@ -2,9 +2,90 @@ package photoprism
import (
"testing"
)
"os"
"github.com/stretchr/testify/assert"
)
func TestNewConverter(t *testing.T) {
NewConverter("storage")
conf := NewTestConfig()
converter := NewConverter(conf.DarktableCli)
assert.IsType(t, &Converter{}, converter)
}
func TestConverter_ConvertToJpeg(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
converter := NewConverter(conf.DarktableCli)
jpegFilename := conf.ImportPath + "/iphone/IMG_6788.jpg"
t.Logf("Testing RAW to JPEG converter with %s", jpegFilename)
imageJpeg,_ := converter.ConvertToJpeg(NewMediaFile(jpegFilename))
infoJpeg, err := imageJpeg.GetExifData()
assert.Equal(t, jpegFilename, imageJpeg.filename)
assert.False(t, infoJpeg == nil || err != nil, "Could not read EXIF data of JPEG image")
assert.Equal(t, "iPhone SE", infoJpeg.CameraModel)
rawFilemame := conf.ImportPath + "/raw/IMG_1435.CR2"
t.Logf("Testing RAW to JPEG converter with %s", rawFilemame)
imageRaw, _ := converter.ConvertToJpeg(NewMediaFile(rawFilemame))
assert.True(t, fileExists(conf.ImportPath + "/raw/IMG_1435.jpg"), "Jpeg file was not found - is Darktable installed?")
assert.NotEqual(t, rawFilemame, imageRaw.filename)
infoRaw, err := imageRaw.GetExifData()
assert.False(t, infoRaw == nil || err != nil, "Could not read EXIF data of RAW image")
assert.Equal(t, "Canon EOS M10", infoRaw.CameraModel)
}
func TestConverter_ConvertAll(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
converter := NewConverter(conf.DarktableCli)
converter.ConvertAll(conf.ImportPath)
jpegFilename := conf.ImportPath + "/raw/IMG_1435.jpg"
assert.True(t, fileExists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
image := NewMediaFile(jpegFilename)
assert.Equal(t, jpegFilename, image.filename, "Filename must be the same")
infoRaw, err := image.GetExifData()
assert.False(t, infoRaw == nil || err != nil, "Could not read EXIF data of RAW image")
assert.Equal(t, "Canon EOS M10", infoRaw.CameraModel, "Camera model should be Canon EOS M10")
existingJpegFilename := conf.ImportPath + "/raw/20140717_154212_1EC48F8489.jpg"
oldHash := fileHash(existingJpegFilename)
os.Remove(existingJpegFilename)
converter.ConvertAll(conf.ImportPath)
newHash := fileHash(existingJpegFilename)
assert.True(t, fileExists(existingJpegFilename), "Jpeg file was not found - is Darktable installed?")
assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same")
}

View file

@ -6,130 +6,114 @@ import (
"log"
"fmt"
"github.com/pkg/errors"
"bytes"
"path"
"sort"
"strings"
)
type Importer struct {
originalsPath string
converter *Converter
removeDotFiles bool
removeExistingFiles bool
removeEmptyDirectories bool
}
func NewImporter(originalsPath string, converter *Converter) *Importer {
func NewImporter(originalsPath string) *Importer {
instance := &Importer{
originalsPath: originalsPath,
converter: converter,
removeDotFiles: true,
removeExistingFiles: true,
removeEmptyDirectories: true,
}
return instance
}
func (importer *Importer) CreateJpegFromRaw(sourcePath string) {
err := filepath.Walk(sourcePath, func(filename string, fileInfo os.FileInfo, err error) error {
func (i *Importer) ImportPhotosFromDirectory(importPath string) {
var directories []string
err := filepath.Walk(importPath, func(filename string, fileInfo os.FileInfo, err error) error {
if err != nil {
log.Print(err.Error())
// log.Print(err.Error())
return nil
}
if fileInfo.IsDir() {
if filename != importPath {
directories = append(directories, filename)
}
return nil
}
if i.removeDotFiles && strings.HasPrefix(filepath.Base(filename), ".") {
os.Remove(filename)
return nil
}
mediaFile := NewMediaFile(filename)
if !mediaFile.Exists() || !mediaFile.IsRaw() {
if !mediaFile.Exists() || !mediaFile.IsPhoto() {
return nil
}
log.Printf("Converting %s \n", filename)
if _, err := importer.converter.ConvertToJpeg(mediaFile); err != nil {
log.Print(err.Error())
}
return nil
})
if err != nil {
log.Print(err.Error())
}
}
func (importer *Importer) ImportJpegFromDirectory(sourcePath string) {
err := filepath.Walk(sourcePath, func(filename string, fileInfo os.FileInfo, err error) error {
if err != nil {
log.Print(err.Error())
return nil
}
if fileInfo.IsDir() {
return nil
}
jpegFile := NewMediaFile(filename)
if !jpegFile.Exists() || !jpegFile.IsJpeg() {
return nil
}
log.Println(jpegFile.GetFilename() + " -> " + jpegFile.GetCanonicalName())
log.Println("Getting related files")
relatedFiles, _ := jpegFile.GetRelatedFiles()
relatedFiles, masterFile, _ := mediaFile.GetRelatedFiles()
for _, relatedMediaFile := range relatedFiles {
log.Println("Processing " + relatedMediaFile.GetFilename())
if destinationFilename, err := importer.GetDestinationFilename(jpegFile, relatedMediaFile); err == nil {
log.Println("Creating directories")
if destinationFilename, err := i.GetDestinationFilename(masterFile, relatedMediaFile); err == nil {
os.MkdirAll(path.Dir(destinationFilename), os.ModePerm)
log.Println("Moving file " + relatedMediaFile.GetFilename())
log.Printf("Moving file %s to %s", relatedMediaFile.GetFilename(), destinationFilename)
relatedMediaFile.Move(destinationFilename)
log.Println("Moved file to " + destinationFilename)
} else {
log.Println("File already exists: " + relatedMediaFile.GetFilename() + " -> " + destinationFilename)
} else if i.removeExistingFiles {
relatedMediaFile.Remove()
log.Printf("Deleted %s (already exists)", relatedMediaFile.GetFilename())
}
}
// mediaFile.Move(importer.originalsPath)
// mediaFile.Move(i.originalsPath)
return nil
})
sort.Slice(directories, func(i, j int) bool {
return len(directories[i]) > len(directories[j])
})
if i.removeEmptyDirectories {
// Remove empty directories from import path
for _, directory := range directories {
if directoryIsEmpty(directory) {
os.Remove(directory)
log.Printf("Deleted empty directory %s", directory)
}
}
}
if err != nil {
log.Print(err.Error())
}
}
func (importer *Importer) GetDestinationFilename(jpegFile *MediaFile, mediaFile *MediaFile) (string, error) {
canonicalName := jpegFile.GetCanonicalName()
func (i *Importer) GetDestinationFilename(masterFile *MediaFile, mediaFile *MediaFile) (string, error) {
canonicalName := masterFile.GetCanonicalName()
fileExtension := mediaFile.GetExtension()
dateCreated := jpegFile.GetDateCreated()
dateCreated := masterFile.GetDateCreated()
// Mon Jan 2 15:04:05 -0700 MST 2006
path := importer.originalsPath + "/" + dateCreated.UTC().Format("2006/01")
pathName := i.originalsPath + "/" + dateCreated.UTC().Format("2006/01")
i := 1
iteration := 1
result := path + "/" + canonicalName + fileExtension
result := pathName + "/" + canonicalName + fileExtension
for FileExists(result) {
if bytes.Compare(mediaFile.GetHash(), Md5Sum(result)) == 0 {
for fileExists(result) {
if mediaFile.GetHash() == fileHash(result) {
return result, errors.New("File already exists")
}
i++
result = path + "/" + canonicalName + "_" + fmt.Sprintf("%02d", i) + fileExtension
// log.Println(result)
iteration++
result = pathName + "/" + canonicalName + "_" + fmt.Sprintf("V%d", iteration) + fileExtension
}
// os.MkdirAll(folderPath, os.ModePerm)
return result, nil
}
func (importer *Importer) MoveRelatedFiles() {
}
}

View file

@ -2,14 +2,37 @@ package photoprism
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestImporter_ImportFromDirectory(t *testing.T) {
config := NewTestConfig()
func TestNewImporter(t *testing.T) {
conf := NewTestConfig()
converter := NewConverter(config.DarktableCli)
importer := NewImporter(config.OriginalsPath, converter)
importer := NewImporter(conf.OriginalsPath)
importer.CreateJpegFromRaw(config.ImportPath)
importer.ImportJpegFromDirectory(config.ImportPath)
assert.IsType(t, &Importer{}, importer)
}
func TestImporter_ImportPhotosFromDirectory(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
importer := NewImporter(conf.OriginalsPath)
importer.ImportPhotosFromDirectory(conf.ImportPath)
}
func TestImporter_GetDestinationFilename(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
importer := NewImporter(conf.OriginalsPath)
rawFile := NewMediaFile(conf.ImportPath + "/raw/IMG_1435.cr2")
filename, err := importer.GetDestinationFilename(rawFile, rawFile)
assert.Empty(t, err)
assert.Equal(t, conf.OriginalsPath + "/2018/02/20180204_170813_B0770443A5F7.cr2", filename)
}

View file

@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/djherbis/times"
"fmt"
"github.com/steakknife/hamming"
)
const (
@ -45,48 +45,52 @@ var FileExtensions = map[string]string {
type MediaFile struct {
filename string
dateCreated time.Time
hash []byte
hash string
fileType string
mimeType string
perceptualHash string
tags []string
exifData *ExifData
}
func NewMediaFile(filename string) *MediaFile {
instance := &MediaFile{filename: filename}
instance := &MediaFile{
filename: filename,
fileType: FileTypeOther,
}
return instance
}
func (mediaFile *MediaFile) GetDateCreated() time.Time {
if !mediaFile.dateCreated.IsZero() {
return mediaFile.dateCreated
func (m *MediaFile) GetDateCreated() time.Time {
if !m.dateCreated.IsZero() {
return m.dateCreated
}
info, err := mediaFile.GetExifData()
info, err := m.GetExifData()
if err == nil {
mediaFile.dateCreated = info.DateTime
m.dateCreated = info.DateTime
return info.DateTime
}
t, err := times.Stat(mediaFile.GetFilename())
t, err := times.Stat(m.GetFilename())
if err != nil {
log.Fatal(err.Error())
}
if t.HasBirthTime() {
mediaFile.dateCreated = t.BirthTime()
m.dateCreated = t.BirthTime()
return t.BirthTime()
}
mediaFile.dateCreated = t.ModTime()
m.dateCreated = t.ModTime()
return t.ModTime()
}
func (mediaFile *MediaFile) GetCameraModel () string {
info, err := mediaFile.GetExifData()
func (m *MediaFile) GetCameraModel () string {
info, err := m.GetExifData()
var result string
@ -97,22 +101,26 @@ func (mediaFile *MediaFile) GetCameraModel () string {
return result
}
func (mediaFile *MediaFile) GetCanonicalName() string {
dateCreated := mediaFile.GetDateCreated().UTC()
cameraModel := strings.Replace(mediaFile.GetCameraModel(), " ", "_", -1)
func (m *MediaFile) GetCanonicalName() string {
dateCreated := m.GetDateCreated().UTC()
//cameraModel := strings.Replace(m.GetCameraModel(), " ", "_", -1)
result := dateCreated.Format("20060102_150405_") + strings.ToUpper(mediaFile.GetHashString()[:8])
result := dateCreated.Format("20060102_150405_") + strings.ToUpper(m.GetHash()[:12])
if cameraModel != "" {
/* if cameraModel != "" {
result = result + "_" + cameraModel
}
} */
return result
}
func (mediaFile *MediaFile) GetPerceptiveHash() (string, error) {
func (m *MediaFile) GetPerceptualHash() (string, error) {
if m.perceptualHash != "" {
return m.perceptualHash, nil
}
hasher := ish.NewDifferenceHash(8, 8)
img, _, err := ish.LoadFile(mediaFile.GetFilename())
img, _, err := ish.LoadFile(m.GetFilename())
if err != nil {
return "", err
@ -124,56 +132,84 @@ func (mediaFile *MediaFile) GetPerceptiveHash() (string, error) {
return "", err
}
dhs := hex.EncodeToString(dh)
m.perceptualHash = hex.EncodeToString(dh)
return dhs, nil
return m.perceptualHash, nil
}
func (mediaFile *MediaFile) GetHash() []byte {
if len(mediaFile.hash) == 0 {
mediaFile.hash = Md5Sum(mediaFile.GetFilename())
func (m *MediaFile) GetPerceptualDistance(perceptualHash string) (int, error) {
var hash1, hash2 []byte
if imageHash, err := m.GetPerceptualHash(); err != nil {
return -1, err
} else {
if decoded, err := hex.DecodeString(imageHash); err != nil {
return -1, err
} else {
hash1 = decoded
}
}
return mediaFile.hash
}
func (mediaFile *MediaFile) GetHashString() string {
return fmt.Sprintf("%x", mediaFile.GetHash())
}
func (mediaFile *MediaFile) GetRelatedFiles() (result []*MediaFile, err error) {
extension := mediaFile.GetExtension()
baseFilename := mediaFile.filename[0:len(mediaFile.filename)-len(extension)]
matches, err := filepath.Glob(baseFilename + "*")
if err != nil {
return result, err
if decoded, err := hex.DecodeString(perceptualHash); err != nil {
return -1, err
} else {
hash2 = decoded
}
for _, filename := range matches {
result = append(result, NewMediaFile(filename))
}
result := hamming.Bytes(hash1, hash2)
return result, nil
}
func (mediaFile *MediaFile) GetFilename() string {
return mediaFile.filename
}
func (mediaFile *MediaFile) SetFilename(filename string) {
mediaFile.filename = filename
}
func (mediaFile *MediaFile) GetMimeType() string {
if mediaFile.mimeType != "" {
return mediaFile.mimeType
func (m *MediaFile) GetHash() string {
if len(m.hash) == 0 {
m.hash = fileHash(m.GetFilename())
}
handle, err := mediaFile.openFile()
return m.hash
}
func (m *MediaFile) GetRelatedFiles() (result []*MediaFile, masterFile *MediaFile, err error) {
extension := m.GetExtension()
baseFilename := m.filename[0:len(m.filename)-len(extension)]
matches, err := filepath.Glob(baseFilename + "*")
if err != nil {
return result, nil, err
}
for _, filename := range matches {
resultFile := NewMediaFile(filename)
if masterFile == nil && resultFile.IsJpeg() {
masterFile = resultFile
} else if resultFile.IsRaw() {
masterFile = resultFile
}
result = append(result, resultFile)
}
return result, masterFile, nil
}
func (m *MediaFile) GetFilename() string {
return m.filename
}
func (m *MediaFile) SetFilename(filename string) {
m.filename = filename
}
func (m *MediaFile) GetMimeType() string {
if m.mimeType != "" {
return m.mimeType
}
handle, err := m.openFile()
if err != nil {
log.Println("Error: Could not open file to determine mime type")
@ -188,17 +224,17 @@ func (mediaFile *MediaFile) GetMimeType() string {
_, err = handle.Read(buffer)
if err != nil {
log.Println("Error: Could not read file to determine mime type: " + mediaFile.GetFilename())
log.Println("Error: Could not read file to determine mime type: " + m.GetFilename())
return ""
}
mediaFile.mimeType = http.DetectContentType(buffer)
m.mimeType = http.DetectContentType(buffer)
return mediaFile.mimeType
return m.mimeType
}
func (mediaFile *MediaFile) openFile() (*os.File, error) {
if handle, err := os.Open(mediaFile.filename); err == nil {
func (m *MediaFile) openFile() (*os.File, error) {
if handle, err := os.Open(m.filename); err == nil {
return handle, nil
} else {
log.Println(err.Error())
@ -206,40 +242,44 @@ func (mediaFile *MediaFile) openFile() (*os.File, error) {
}
}
func (mediaFile *MediaFile) Exists() bool {
return FileExists(mediaFile.GetFilename())
func (m *MediaFile) Exists() bool {
return fileExists(m.GetFilename())
}
func (mediaFile *MediaFile) Move(newFilename string) error {
if err := os.Rename(mediaFile.filename, newFilename); err != nil {
func (m *MediaFile) Remove() error {
return os.Remove(m.GetFilename())
}
func (m *MediaFile) Move(newFilename string) error {
if err := os.Rename(m.filename, newFilename); err != nil {
return err
}
mediaFile.filename = newFilename
m.filename = newFilename
return nil
}
func (mediaFile *MediaFile) GetExtension() string {
return strings.ToLower(filepath.Ext(mediaFile.filename))
func (m *MediaFile) GetExtension() string {
return strings.ToLower(filepath.Ext(m.filename))
}
func (mediaFile *MediaFile) IsJpeg() bool {
return mediaFile.GetMimeType() == MimeTypeJpeg
func (m *MediaFile) IsJpeg() bool {
return m.GetMimeType() == MimeTypeJpeg
}
func (mediaFile *MediaFile) HasType(typeString string) bool {
func (m *MediaFile) HasType(typeString string) bool {
if typeString == FileTypeJpeg {
return mediaFile.IsJpeg()
return m.IsJpeg()
}
return FileExtensions[mediaFile.GetExtension()] == typeString
return FileExtensions[m.GetExtension()] == typeString
}
func (mediaFile *MediaFile) IsRaw() bool {
return mediaFile.HasType(FileTypeRaw)
func (m *MediaFile) IsRaw() bool {
return m.HasType(FileTypeRaw)
}
func (mediaFile *MediaFile) IsPhoto() bool {
return mediaFile.IsJpeg() || mediaFile.IsRaw()
func (m *MediaFile) IsPhoto() bool {
return m.IsJpeg() || m.IsRaw()
}

View file

@ -6,36 +6,36 @@ import (
"time"
"errors"
"strings"
"log"
)
type ExifData struct {
DateTime time.Time
CameraModel string
UniqueID string
Lat float64
Long float64
Thumbnail []byte
}
func (mediaFile *MediaFile) GetExifData() (*ExifData, error) {
if mediaFile.exifData != nil {
log.Printf("GetExifData() Cache Hit %s", mediaFile.filename)
return mediaFile.exifData, nil
func (m *MediaFile) GetExifData() (*ExifData, error) {
if m == nil {
return nil, errors.New("media file is null")
}
log.Printf("GetExifData() Cache Miss %s", mediaFile.filename)
if !mediaFile.IsJpeg() {
// EXIF only works for JPEG
return nil, errors.New("MediaFile is not a JPEG")
if m.exifData != nil {
return m.exifData, nil
}
mediaFile.exifData = &ExifData{}
if !m.IsPhoto() {
return nil, errors.New("not a JPEG or Raw file")
}
log.Printf("GetExifData() Open File %s", mediaFile.filename)
file, err := mediaFile.openFile()
m.exifData = &ExifData{}
file, err := m.openFile()
if err != nil {
return mediaFile.exifData, err
return m.exifData, err
}
defer file.Close()
@ -45,18 +45,29 @@ func (mediaFile *MediaFile) GetExifData() (*ExifData, error) {
x, err := exif.Decode(file)
if err != nil {
return mediaFile.exifData, err
return m.exifData, err
}
camModel, _ := x.Get(exif.Model)
mediaFile.exifData.CameraModel = strings.Replace(camModel.String(), "\"", "", -1)
if camModel, err := x.Get(exif.Model); err == nil {
m.exifData.CameraModel = strings.Replace(camModel.String(), "\"", "", -1)
}
tm, _ := x.DateTime()
mediaFile.exifData.DateTime = tm
if tm, err := x.DateTime(); err == nil {
m.exifData.DateTime = tm
}
lat, long, _ := x.LatLong()
mediaFile.exifData.Lat = lat
mediaFile.exifData.Long = long
if lat, long, err := x.LatLong(); err == nil {
m.exifData.Lat = lat
m.exifData.Long = long
}
return mediaFile.exifData, nil
if thumbnail, err := x.JpegThumbnail(); err == nil {
m.exifData.Thumbnail = thumbnail
}
if uniqueId, err := x.Get(exif.ImageUniqueID); err == nil {
m.exifData.UniqueID = uniqueId.String()
}
return m.exifData, nil
}

View file

@ -2,30 +2,31 @@ package photoprism
import (
"testing"
"fmt"
"github.com/stretchr/testify/assert"
)
func TestImage_GetExifData(t *testing.T) {
config := NewTestConfig()
func TestMediaFile_GetExifData(t *testing.T) {
conf := NewTestConfig()
converter := NewConverter(config.DarktableCli)
conf.InitializeTestData(t)
image1 := NewMediaFile("storage/import/IMG_9083.jpg")
image1 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
info1, _ := image1.GetExifData()
info, err := image1.GetExifData()
fmt.Printf("%+v\n", info1)
assert.Empty(t, err);
image2,_ := converter.ConvertToJpeg(NewMediaFile("storage/import/IMG_5901.JPG"))
assert.IsType(t, &ExifData{}, info)
info2, _ := image2.GetExifData()
assert.Equal(t, "iPhone SE", info.CameraModel)
fmt.Printf("%+v\n", info2)
image2 := NewMediaFile(conf.ImportPath + "/raw/IMG_1435.cr2")
image3, _ := converter.ConvertToJpeg(NewMediaFile("storage/import/IMG_9087.CR2"))
info, err = image2.GetExifData()
info3, _ := image3.GetExifData()
assert.Empty(t, err)
fmt.Printf("%+v\n", info3)
assert.IsType(t, &ExifData{}, info)
assert.Equal(t, "Canon EOS M10", info.CameraModel)
}

View file

@ -2,61 +2,89 @@ package photoprism
import (
"testing"
"fmt"
"github.com/stretchr/testify/assert"
)
func TestMediaFile_ConvertToJpeg(t *testing.T) {
converterCommand := "/Applications/darktable.app/Contents/MacOS/darktable-cli"
converter := NewConverter(converterCommand)
image1,_ := converter.ConvertToJpeg(NewMediaFile("storage/import/IMG_5901.JPG"))
info1, _ := image1.GetExifData()
fmt.Printf("%+v\n", info1)
image2, _ := converter.ConvertToJpeg(NewMediaFile("storage/import/IMG_9087.CR2"))
info2, _ := image2.GetExifData()
fmt.Printf("%+v\n", info2)
}
func TestMediaFile_FindRelatedImages(t *testing.T) {
image := NewMediaFile("storage/import/IMG_9079.jpg")
conf := NewTestConfig()
related, err := image.GetRelatedFiles()
conf.InitializeTestData(t)
if err != nil {
t.Error(err)
}
mediaFile := NewMediaFile(conf.ImportPath + "/raw/20140717_154212_1EC48F8489.cr2")
expectedBaseFilename := conf.ImportPath + "/raw/20140717_154212_1EC48F8489"
related, _, err := mediaFile.GetRelatedFiles()
assert.Empty(t, err)
assert.Len(t, related, 3)
for _, result := range related {
info, _ := result.GetExifData()
fmt.Printf("%s %+v\n", result.GetFilename(), info)
filename := result.GetFilename()
extension := result.GetExtension()
baseFilename := filename[0:len(filename)-len(extension)]
assert.Equal(t, expectedBaseFilename, baseFilename)
}
}
func TestMediaFile_GetPerceptiveHash(t *testing.T) {
image := NewMediaFile("storage/import/IMG_9079.jpg")
conf := NewTestConfig()
hash, _ := image.GetPerceptiveHash()
fmt.Printf("Perceptive Hash (large): %s\n", hash)
conf.InitializeTestData(t)
image2 := NewMediaFile("storage/import/IMG_9079_small.jpg")
mediaFile1 := NewMediaFile(conf.ImportPath + "/20130203_193332_0AE340D280.jpg")
hash2, _ := image2.GetPerceptiveHash()
fmt.Printf("Perceptive Hash (small): %s\n", hash2)
hash1, _ := mediaFile1.GetPerceptualHash()
assert.Equal(t, "66debc383325d3bd", hash1)
mediaFile2 := NewMediaFile(conf.ImportPath + "/20130203_193332_0AE340D280_V2.jpg")
hash2, _ := mediaFile2.GetPerceptualHash()
assert.Equal(t, "e6debc393325c3b9", hash2)
distance, _ := mediaFile1.GetPerceptualDistance(hash2)
assert.Equal(t, 4, distance)
mediaFile3 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
hash3, _ := mediaFile3.GetPerceptualHash()
assert.Equal(t, "f1e2858b171d3e78", hash3)
distance, _ = mediaFile1.GetPerceptualDistance(hash3)
assert.Equal(t, 33, distance)
}
func TestMediaFile_GetMimeType(t *testing.T) {
image1 := NewMediaFile("storage/import/IMG_9083.jpg")
conf := NewTestConfig()
fmt.Println("MimeType: " + image1.GetMimeType())
conf.InitializeTestData(t)
image2 := NewMediaFile("storage/import/IMG_9082.CR2")
image1 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
fmt.Println("MimeType: " + image2.GetMimeType())
assert.Equal(t, "image/jpeg", image1.GetMimeType())
image2 := NewMediaFile(conf.ImportPath + "/raw/20140717_154212_1EC48F8489.cr2")
assert.Equal(t, "application/octet-stream", image2.GetMimeType())
}
func TestMediaFile_Exists(t *testing.T) {
conf := NewTestConfig()
mediaFile := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
assert.True(t, mediaFile.Exists())
mediaFile = NewMediaFile(conf.ImportPath + "/iphone/IMG_6788_XYZ.JPG")
assert.False(t, mediaFile.Exists())
}

2
testdata/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

139
util.go
View file

@ -6,16 +6,22 @@ import (
"os/user"
"path/filepath"
"os"
"crypto/md5"
"crypto/sha512"
"io"
"strings"
"archive/zip"
"log"
"fmt"
"net/http"
"encoding/hex"
)
func GetRandomInt(min, max int) int {
func getRandomInt(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
func GetExpandedFilename(filename string) string {
func getExpandedFilename(filename string) string {
usr, _ := user.Current()
dir := usr.HomeDir
@ -28,28 +34,141 @@ func GetExpandedFilename(filename string) string {
return result
}
func FileExists (filename string) bool {
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
func Md5Sum (filename string) []byte {
func fileHash(filename string) string {
var result []byte
file, err := os.Open(filename)
if err != nil {
return result
return ""
}
defer file.Close()
hash := md5.New()
hash := sha512.New512_224()
if _, err := io.Copy(hash, file); err != nil {
return result
return ""
}
return hash.Sum(result)
}
return hex.EncodeToString(hash.Sum(result))
}
func directoryIsEmpty(path string) bool {
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
return true
}
return false
}
func unzip(src, dest string) ([]string, error) {
var filenames []string
r, err := zip.OpenReader(src)
if err != nil {
return filenames, err
}
defer r.Close()
for _, f := range r.File {
// Skip directories like __OSX
if strings.HasPrefix(f.Name, "__") {
continue
}
rc, err := f.Open()
if err != nil {
return filenames, err
}
defer rc.Close()
// Store filename/path for returning and using later on
fpath := filepath.Join(dest, f.Name)
filenames = append(filenames, fpath)
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(fpath, os.ModePerm)
} else {
// Make File
var fdir string
if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {
fdir = fpath[:lastIndex]
}
err = os.MkdirAll(fdir, os.ModePerm)
if err != nil {
log.Fatal(err)
return filenames, err
}
f, err := os.OpenFile(
fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return filenames, err
}
defer f.Close()
_, err = io.Copy(f, rc)
if err != nil {
return filenames, err
}
}
}
return filenames, nil
}
func downloadFile(filepath string, url string) (err error) {
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
// Writer the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}

View file

@ -7,7 +7,7 @@ func TestGetRandomInt(t *testing.T) {
max := 50
for i := 0; i < 10; i++ {
result := GetRandomInt(min, max)
result := getRandomInt(min, max)
if result > max {
t.Errorf("Random result must not be bigger than %d", max)