Convert: Add --force flag to replace JPEGs in the sidecar folder #2214

This commit is contained in:
Michael Mayer
2022-04-03 12:26:07 +02:00
parent 0838a71e6e
commit 4be948c774
15 changed files with 68 additions and 31 deletions

View File

@@ -16,9 +16,15 @@ import (
// ConvertCommand registers the convert cli command. // ConvertCommand registers the convert cli command.
var ConvertCommand = cli.Command{ var ConvertCommand = cli.Command{
Name: "convert", Name: "convert",
Usage: "Converts files in other formats to JPEG and AVC", Usage: "Converts files in other formats to JPEG and AVC as needed",
ArgsUsage: "[ORIGINALS SUB-FOLDER]", ArgsUsage: "[originals folder]",
Action: convertAction, Flags: []cli.Flag{
cli.BoolFlag{
Name: "force, f",
Usage: "replace existing JPEG files in the sidecar folder",
},
},
Action: convertAction,
} }
// convertAction converts originals in other formats to JPEG and AVC sidecar files. // convertAction converts originals in other formats to JPEG and AVC sidecar files.
@@ -52,7 +58,8 @@ func convertAction(ctx *cli.Context) error {
w := service.Convert() w := service.Convert()
if err := w.Start(convertPath); err != nil { // Start file conversion.
if err := w.Start(convertPath, ctx.Bool("force")); err != nil {
log.Error(err) log.Error(err)
} }

View File

@@ -19,7 +19,7 @@ var CopyCommand = cli.Command{
Name: "cp", Name: "cp",
Aliases: []string{"copy"}, Aliases: []string{"copy"},
Usage: "Copies media files to originals", Usage: "Copies media files to originals",
ArgsUsage: "[PATH]", ArgsUsage: "[path]",
Action: copyAction, Action: copyAction,
} }

View File

@@ -53,7 +53,7 @@ var FacesCommand = cli.Command{
{ {
Name: "index", Name: "index",
Usage: "Searches originals for faces", Usage: "Searches originals for faces",
ArgsUsage: "[ORIGINALS SUB-FOLDER]", ArgsUsage: "[originals folder]",
Action: facesIndexAction, Action: facesIndexAction,
}, },
{ {

View File

@@ -19,7 +19,7 @@ var ImportCommand = cli.Command{
Name: "mv", Name: "mv",
Aliases: []string{"import"}, Aliases: []string{"import"},
Usage: "Moves media files to originals", Usage: "Moves media files to originals",
ArgsUsage: "[PATH]", ArgsUsage: "[path]",
Action: importAction, Action: importAction,
} }

View File

@@ -21,7 +21,7 @@ import (
var IndexCommand = cli.Command{ var IndexCommand = cli.Command{
Name: "index", Name: "index",
Usage: "Indexes original media files", Usage: "Indexes original media files",
ArgsUsage: "[ORIGINALS SUB-FOLDER]", ArgsUsage: "[originals folder]",
Flags: indexFlags, Flags: indexFlags,
Action: indexAction, Action: indexAction,
} }

View File

@@ -31,7 +31,7 @@ var RestoreCommand = cli.Command{
Name: "restore", Name: "restore",
Description: restoreDescription, Description: restoreDescription,
Usage: "Restores the index from an SQL dump and optionally albums from YAML files", Usage: "Restores the index from an SQL dump and optionally albums from YAML files",
ArgsUsage: "[FILENAME]", ArgsUsage: "[filename.sql]",
Flags: restoreFlags, Flags: restoreFlags,
Action: restoreAction, Action: restoreAction,
} }

View File

@@ -73,7 +73,7 @@ var UsersCommand = cli.Command{
Name: "delete", Name: "delete",
Usage: "Removes an existing user", Usage: "Removes an existing user",
Action: usersDeleteAction, Action: usersDeleteAction,
ArgsUsage: "[USERNAME]", ArgsUsage: "[username]",
}, },
}, },
} }

View File

@@ -44,7 +44,7 @@ func NewConvert(conf *config.Config) *Convert {
} }
// Start converts all files in a directory to JPEG if possible. // Start converts all files in a directory to JPEG if possible.
func (c *Convert) Start(path string) (err error) { func (c *Convert) Start(path string, force bool) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("convert: %s (panic)\nstack: %s", r, debug.Stack()) err = fmt.Errorf("convert: %s (panic)\nstack: %s", r, debug.Stack())
@@ -114,6 +114,7 @@ func (c *Convert) Start(path string) (err error) {
done[fileName] = fs.Processed done[fileName] = fs.Processed
jobs <- ConvertJob{ jobs <- ConvertJob{
force: force,
file: f, file: f,
convert: c, convert: c,
} }
@@ -245,7 +246,7 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri
} }
// ToJpeg converts a single image file to JPEG if possible. // ToJpeg converts a single image file to JPEG if possible.
func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) { func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
if f == nil { if f == nil {
return nil, fmt.Errorf("convert: file is nil - you might have found a bug") return nil, fmt.Errorf("convert: file is nil - you might have found a bug")
} }
@@ -262,17 +263,26 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
mediaFile, err := NewMediaFile(jpegName) mediaFile, err := NewMediaFile(jpegName)
// Replace existing sidecar if "force" is true.
if err == nil && mediaFile.IsJpeg() { if err == nil && mediaFile.IsJpeg() {
return mediaFile, nil if force && mediaFile.InSidecar() {
if err := mediaFile.Remove(); err != nil {
return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", mediaFile.RootRelName(), err)
} else {
log.Infof("convert: replacing %s", sanitize.Log(mediaFile.RootRelName()))
}
} else {
return mediaFile, nil
}
} else {
jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
} }
if !c.conf.SidecarWritable() { if !c.conf.SidecarWritable() {
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", f.RelName(c.conf.OriginalsPath())) return nil, fmt.Errorf("convert: disabled in read only mode (%s)", f.RelName(c.conf.OriginalsPath()))
} }
jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
fileName := f.RelName(c.conf.OriginalsPath()) fileName := f.RelName(c.conf.OriginalsPath())
xmpName := fs.FormatXMP.Find(f.FileName(), false) xmpName := fs.FormatXMP.Find(f.FileName(), false)
event.Publish("index.converting", event.Data{ event.Publish("index.converting", event.Data{
@@ -285,7 +295,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
start := time.Now() start := time.Now()
if f.IsImageOther() { if f.IsImageOther() {
log.Infof("%s: converting %s to %s", f.FileType(), fileName, fs.FormatJpeg) log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), f.FileType())
_, err = thumb.Jpeg(f.FileName(), jpegName, f.Orientation()) _, err = thumb.Jpeg(f.FileName(), jpegName, f.Orientation())
@@ -293,7 +303,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
return nil, err return nil, err
} }
log.Infof("%s: created %s [%s]", f.FileType(), filepath.Base(jpegName), time.Since(start)) log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), f.FileType())
return NewMediaFile(jpegName) return NewMediaFile(jpegName)
} }
@@ -321,7 +331,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
cmd.Stdout = &out cmd.Stdout = &out
cmd.Stderr = &stderr cmd.Stderr = &stderr
log.Infof("%s: converting %s to %s", filepath.Base(cmd.Path), fileName, fs.FormatJpeg) log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
// Log exact command for debugging in trace mode. // Log exact command for debugging in trace mode.
log.Trace(cmd.String()) log.Trace(cmd.String())
@@ -335,7 +345,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
} }
} }
log.Infof("%s: created %s [%s]", filepath.Base(cmd.Path), filepath.Base(jpegName), time.Since(start)) log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
return NewMediaFile(jpegName) return NewMediaFile(jpegName)
} }

View File

@@ -41,7 +41,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
jpegFile, err := convert.ToJpeg(mf) jpegFile, err := convert.ToJpeg(mf, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -68,7 +68,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
imageJpeg, err := convert.ToJpeg(mf) imageJpeg, err := convert.ToJpeg(mf, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -91,7 +91,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatalf("%s for %s", err.Error(), rawFilename) t.Fatalf("%s for %s", err.Error(), rawFilename)
} }
imageRaw, err := convert.ToJpeg(rawMediaFile) imageRaw, err := convert.ToJpeg(rawMediaFile, false)
if err != nil { if err != nil {
t.Fatalf("%s for %s", err.Error(), rawFilename) t.Fatalf("%s for %s", err.Error(), rawFilename)
@@ -206,7 +206,7 @@ func TestConvert_Start(t *testing.T) {
convert := NewConvert(conf) convert := NewConvert(conf)
err := convert.Start(conf.ImportPath()) err := convert.Start(conf.ImportPath(), false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -234,7 +234,7 @@ func TestConvert_Start(t *testing.T) {
_ = os.Remove(existingJpegFilename) _ = os.Remove(existingJpegFilename)
if err := convert.Start(conf.ImportPath()); err != nil { if err := convert.Start(conf.ImportPath(), false); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -7,6 +7,7 @@ import (
) )
type ConvertJob struct { type ConvertJob struct {
force bool
file *MediaFile file *MediaFile
convert *Convert convert *Convert
} }
@@ -26,7 +27,8 @@ func ConvertWorker(jobs <-chan ConvertJob) {
case job.file.IsVideo(): case job.file.IsVideo():
_, _ = job.convert.ToJson(job.file) _, _ = job.convert.ToJson(job.file)
if _, err := job.convert.ToJpeg(job.file); err != nil { // Create JPEG preview and AVC encoded version for videos.
if _, err := job.convert.ToJpeg(job.file, job.force); err != nil {
logError(err, job) logError(err, job)
} else if metaData := job.file.MetaData(); metaData.CodecAvc() { } else if metaData := job.file.MetaData(); metaData.CodecAvc() {
continue continue
@@ -34,7 +36,7 @@ func ConvertWorker(jobs <-chan ConvertJob) {
logError(err, job) logError(err, job)
} }
default: default:
if _, err := job.convert.ToJpeg(job.file); err != nil { if _, err := job.convert.ToJpeg(job.file, job.force); err != nil {
logError(err, job) logError(err, job)
} }
} }

View File

@@ -135,7 +135,7 @@ func ImportWorker(jobs <-chan ImportJob) {
// Create JPEG sidecar for media files in other formats so that thumbnails can be created. // Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() { if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := imp.convert.ToJpeg(f); err != nil { if jpegFile, err := imp.convert.ToJpeg(f, false); err != nil {
log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), sanitize.Log(f.RootRelName())) log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), sanitize.Log(f.RootRelName()))
continue continue
} else { } else {

View File

@@ -42,7 +42,7 @@ func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexR
// Create JPEG sidecar for media files in other formats so that thumbnails can be created. // Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() { if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f); err != nil { if jpg, err := ind.convert.ToJpeg(f, false); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error()) result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed result.Status = IndexFailed
return result return result

View File

@@ -75,7 +75,7 @@ func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result Inde
// Create JPEG sidecar for media files in other formats so that thumbnails can be created. // Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() { if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f); err != nil { if jpg, err := ind.convert.ToJpeg(f, false); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error()) result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed result.Status = IndexFailed
return result return result

View File

@@ -752,7 +752,17 @@ func (m *MediaFile) IsXMP() bool {
return m.FileType() == fs.FormatXMP return m.FileType() == fs.FormatXMP
} }
// IsSidecar returns true if this is a sidecar file (containing metadata). // InOriginals checks if the file is stored in the 'originals' folder.
func (m *MediaFile) InOriginals() bool {
return m.Root() == entity.RootOriginals
}
// InSidecar checks if the file is stored in the 'sidecar' folder.
func (m *MediaFile) InSidecar() bool {
return m.Root() == entity.RootSidecar
}
// IsSidecar checks if the file is a metadata sidecar file, independent of the storage location.
func (m *MediaFile) IsSidecar() bool { func (m *MediaFile) IsSidecar() bool {
return m.MediaType() == fs.MediaSidecar return m.MediaType() == fs.MediaSidecar
} }

View File

@@ -325,7 +325,15 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
convert := NewConvert(conf) convert := NewConvert(conf)
jpeg, err := convert.ToJpeg(img) // Create JPEG.
jpeg, err := convert.ToJpeg(img, false)
if err != nil {
t.Fatal(err)
}
// Replace JPEG.
jpeg, err = convert.ToJpeg(img, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)