Group related files #283

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-14 14:28:47 +02:00
parent 855781658b
commit 96ec67f868
15 changed files with 96 additions and 45 deletions

View file

@ -16,5 +16,5 @@ library:
rescan: false
raw: false
thumbs: true
related: true
group: true
move: false

View file

@ -43,9 +43,9 @@
<v-checkbox
@change="save"
class="ma-0 pa-0"
v-model="settings.library.related"
v-model="settings.library.group"
color="secondary-dark"
:label="labels.related"
:label="labels.group"
hint="Files with sequential names like 'IMG_1234 (2)' or 'IMG_1234 copy 2' belong to the same photo."
prepend-icon="file_copy"
persistent-hint
@ -286,7 +286,7 @@
thumbs: this.$gettext("Create thumbnails"),
raw: this.$gettext("Convert RAW files"),
move: this.$gettext("Remove imported files"),
related: this.$gettext("Find related files"),
group: this.$gettext("Group related files"),
},
};
},

View file

@ -23,7 +23,7 @@ type LibrarySettings struct {
CompleteRescan bool `json:"rescan" yaml:"rescan"`
ConvertRaw bool `json:"raw" yaml:"raw"`
CreateThumbs bool `json:"thumbs" yaml:"thumbs"`
FindRelated bool `json:"related" yaml:"related"`
GroupRelated bool `json:"group" yaml:"group"`
MoveImported bool `json:"move" yaml:"move"`
}
@ -70,7 +70,7 @@ func NewSettings() *Settings {
CompleteRescan: false,
ConvertRaw: false,
CreateThumbs: true,
FindRelated: true,
GroupRelated: true,
MoveImported: false,
},
}

View file

@ -94,6 +94,7 @@ func NewTestConfig() *Config {
log.SetLevel(logrus.DebugLevel)
c := &Config{params: NewTestParams()}
c.initSettings()
err := c.Init(context.Background())
if err != nil {
log.Fatalf("failed init config: %v", err)

View file

@ -16,5 +16,5 @@ library:
rescan: false
raw: false
thumbs: true
related: true
group: true
move: false

View file

@ -102,7 +102,7 @@ func (c *Convert) ConvertCommand(image *MediaFile, jpegName string, xmpName stri
result = exec.Command(c.conf.DarktableBin(), image.fileName, jpegName)
}
} else {
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", image.Base())
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", image.Base(c.conf.Settings().Library.GroupRelated))
}
} else if image.IsHEIF() {
result = exec.Command(c.conf.HeifConvertBin(), image.fileName, jpegName)
@ -123,7 +123,7 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
return image, nil
}
base := image.AbsBase()
base := image.AbsBase(c.conf.Settings().Library.GroupRelated)
jpegName := base + ".jpg"

View file

@ -121,7 +121,7 @@ func (imp *Import) Start(opt ImportOptions) {
return nil
}
related, err := mf.RelatedFiles()
related, err := mf.RelatedFiles(imp.conf.Settings().Library.GroupRelated)
if err != nil {
event.Error(fmt.Sprintf("import: %s", err.Error()))

View file

@ -93,7 +93,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
}
related, err := importedMainFile.RelatedFiles()
related, err := importedMainFile.RelatedFiles(imp.conf.Settings().Library.GroupRelated)
if err != nil {
log.Errorf("import: could not index \"%s\" (%s)", destinationMainFilename, err.Error())

View file

@ -120,7 +120,7 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
return nil
}
related, err := mf.RelatedFiles()
related, err := mf.RelatedFiles(ind.conf.Settings().Library.GroupRelated)
if err != nil {
log.Warnf("index: %s", err.Error())

View file

@ -61,7 +61,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
var locKeywords []string
labels := classify.Labels{}
fileBase := m.Base()
fileBase := m.Base(ind.conf.Settings().Library.GroupRelated)
filePath := m.RelativePath(ind.originalsPath())
fileName := m.RelativeName(ind.originalsPath())
fileHash := ""

View file

@ -269,8 +269,8 @@ func (m *MediaFile) EditedName() string {
}
// RelatedFiles returns files which are related to this file.
func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
baseFilename := m.AbsBase()
func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) {
baseFilename := m.AbsBase(stripSequence)
// escape any meta characters in the file name
baseFilename = regexp.QuoteMeta(baseFilename)
matches, err := filepath.Glob(baseFilename + "*")
@ -357,12 +357,12 @@ func (m MediaFile) RelativePath(directory string) string {
}
// RelativeBase returns the relative filename.
func (m MediaFile) RelativeBase(directory string) string {
func (m MediaFile) RelativeBase(directory string, stripSequence bool) string {
if relativePath := m.RelativePath(directory); relativePath != "" {
return relativePath + string(os.PathSeparator) + m.Base()
return filepath.Join(relativePath, m.Base(stripSequence))
}
return m.Base()
return m.Base(stripSequence)
}
// Directory returns the directory
@ -371,13 +371,13 @@ func (m MediaFile) Directory() string {
}
// Base returns the filename base without any extensions and path.
func (m MediaFile) Base() string {
return fs.Base(m.FileName())
func (m MediaFile) Base(stripSequence bool) string {
return fs.Base(m.FileName(), stripSequence)
}
// AbsBase returns the directory and base filename without any extensions.
func (m MediaFile) AbsBase() string {
return m.Directory() + string(os.PathSeparator) + m.Base()
func (m MediaFile) AbsBase(stripSequence bool) string {
return fs.AbsBase(m.FileName(), stripSequence)
}
// MimeType returns the mime type.
@ -573,7 +573,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
return m, nil
}
jpegFilename := fmt.Sprintf("%s.%s", m.AbsBase(), fs.TypeJpeg)
jpegFilename := fmt.Sprintf("%s.%s", m.AbsBase(false), fs.TypeJpeg)
if !fs.FileExists(jpegFilename) {
return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename)
@ -712,11 +712,11 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
defer func() {
switch count {
case 0:
log.Info(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Base())))
log.Info(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Base(false))))
case 1:
log.Info(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Base())))
log.Info(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Base(false))))
default:
log.Info(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Base())))
log.Info(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Base(false))))
}
}()

View file

@ -257,7 +257,7 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
expectedBaseFilename := conf.ExamplesPath() + "/canon_eos_6d"
related, err := mediaFile.RelatedFiles()
related, err := mediaFile.RelatedFiles(true)
assert.Nil(t, err)
@ -283,7 +283,7 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
expectedBaseFilename := conf.ExamplesPath() + "/iphone_7"
related, err := mediaFile.RelatedFiles()
related, err := mediaFile.RelatedFiles(true)
assert.Nil(t, err)
@ -310,7 +310,7 @@ func TestMediaFile_RelatedFiles_Ordering(t *testing.T) {
assert.Nil(t, err)
related, err := mediaFile.RelatedFiles()
related, err := mediaFile.RelatedFiles(true)
assert.Nil(t, err)
@ -393,15 +393,15 @@ func TestMediaFile_RelativeBasename(t *testing.T) {
assert.Nil(t, err)
t.Run("directory with end slash", func(t *testing.T) {
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/")
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/", true)
assert.Equal(t, "examples/tree_white", basename)
})
t.Run("directory without end slash", func(t *testing.T) {
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources")
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources", true)
assert.Equal(t, "examples/tree_white", basename)
})
t.Run("directory equals example path", func(t *testing.T) {
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/examples/")
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/examples/", true)
assert.Equal(t, "tree_white", basename)
})
@ -423,21 +423,21 @@ func TestMediaFile_Basename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/limes.jpg")
assert.Nil(t, err)
assert.Equal(t, "limes", mediaFile.Base())
assert.Equal(t, "limes", mediaFile.Base(true))
})
t.Run("/IMG_4120 copy.JPG", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 copy.JPG")
assert.Nil(t, err)
assert.Equal(t, "IMG_4120", mediaFile.Base())
assert.Equal(t, "IMG_4120", mediaFile.Base(true))
})
t.Run("/IMG_4120 (1).JPG", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 (1).JPG")
assert.Nil(t, err)
assert.Equal(t, "IMG_4120", mediaFile.Base())
assert.Equal(t, "IMG_4120", mediaFile.Base(true))
})
}

View file

@ -32,7 +32,7 @@ func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error)
// Group results by directory and base name
for i, file := range files {
k := fs.AbsBase(file.RemoteName)
k := fs.AbsBase(file.RemoteName, s.conf.Settings().Library.GroupRelated)
result[k] = append(result[k], file)
@ -124,7 +124,7 @@ func (s *Sync) download(a entity.Account) (complete bool, err error) {
continue
}
related, err := mf.RelatedFiles()
related, err := mf.RelatedFiles(s.conf.Settings().Library.GroupRelated)
if err != nil {
log.Warnf("sync: %s", err.Error())

View file

@ -1,13 +1,12 @@
package fs
import (
"os"
"path/filepath"
"strings"
)
// Base returns the filename base without any extensions and path.
func Base(fileName string) string {
func Base(fileName string, stripSequence bool) string {
basename := filepath.Base(fileName)
if end := strings.Index(basename, "."); end != -1 {
@ -15,6 +14,11 @@ func Base(fileName string) string {
basename = basename[:end]
}
if !stripSequence {
return basename
}
// common sequential naming schemes
if end := strings.Index(basename, " ("); end != -1 {
// copies created by Chrome & Windows, example: IMG_1234 (2)
basename = basename[:end]
@ -27,6 +31,6 @@ func Base(fileName string) string {
}
// AbsBase returns the directory and base filename without any extensions.
func AbsBase(fileName string) string {
return filepath.Dir(fileName) + string(os.PathSeparator) + Base(fileName)
func AbsBase(fileName string, stripSequence bool) string {
return filepath.Join(filepath.Dir(fileName), Base(fileName, stripSequence))
}

View file

@ -7,13 +7,59 @@ import (
)
func TestBase(t *testing.T) {
result := Base("/testdata/test.jpg")
t.Run("Test.jpg", func(t *testing.T) {
result := Base("/testdata/Test.jpg", true)
assert.Equal(t, "Test", result)
})
assert.Equal(t, "test", result)
t.Run("Test.3453453.jpg", func(t *testing.T) {
result := Base("/testdata/Test.3453453.jpg", true)
assert.Equal(t, "Test", result)
})
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := Base("/testdata/Test copy 3.jpg", true)
assert.Equal(t, "Test", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := Base("/testdata/Test (3).jpg", true)
assert.Equal(t, "Test", result)
})
t.Run("Test.jpg", func(t *testing.T) {
result := Base("/testdata/Test.jpg", false)
assert.Equal(t, "Test", result)
})
t.Run("Test.3453453.jpg", func(t *testing.T) {
result := Base("/testdata/Test.3453453.jpg", false)
assert.Equal(t, "Test", result)
})
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := Base("/testdata/Test copy 3.jpg", false)
assert.Equal(t, "Test copy 3", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := Base("/testdata/Test (3).jpg", false)
assert.Equal(t, "Test (3)", result)
})
}
func TestBaseAbs(t *testing.T) {
result := AbsBase("/testdata/test.jpg")
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := AbsBase("/testdata/Test (4).jpg", true)
assert.Equal(t, "/testdata/Test", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := AbsBase("/testdata/Test (4).jpg", false)
assert.Equal(t, "/testdata/Test (4)", result)
})
assert.Equal(t, "/testdata/test", result)
}