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.
var ConvertCommand = cli.Command{
Name: "convert",
Usage: "Converts files in other formats to JPEG and AVC",
ArgsUsage: "[ORIGINALS SUB-FOLDER]",
Action: convertAction,
Usage: "Converts files in other formats to JPEG and AVC as needed",
ArgsUsage: "[originals folder]",
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.
@ -52,7 +58,8 @@ func convertAction(ctx *cli.Context) error {
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,7 +73,7 @@ var UsersCommand = cli.Command{
Name: "delete",
Usage: "Removes an existing user",
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.
func (c *Convert) Start(path string) (err error) {
func (c *Convert) Start(path string, force bool) (err error) {
defer func() {
if r := recover(); r != nil {
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
jobs <- ConvertJob{
force: force,
file: f,
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.
func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
if f == nil {
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)
// Replace existing sidecar if "force" is true.
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() {
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())
xmpName := fs.FormatXMP.Find(f.FileName(), false)
event.Publish("index.converting", event.Data{
@ -285,7 +295,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
start := time.Now()
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())
@ -293,7 +303,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
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)
}
@ -321,7 +331,7 @@ func (c *Convert) ToJpeg(f *MediaFile) (*MediaFile, error) {
cmd.Stdout = &out
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.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)
}

View file

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

View file

@ -7,6 +7,7 @@ import (
)
type ConvertJob struct {
force bool
file *MediaFile
convert *Convert
}
@ -26,7 +27,8 @@ func ConvertWorker(jobs <-chan ConvertJob) {
case job.file.IsVideo():
_, _ = 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)
} else if metaData := job.file.MetaData(); metaData.CodecAvc() {
continue
@ -34,7 +36,7 @@ func ConvertWorker(jobs <-chan ConvertJob) {
logError(err, job)
}
default:
if _, err := job.convert.ToJpeg(job.file); err != nil {
if _, err := job.convert.ToJpeg(job.file, job.force); err != nil {
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.
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()))
continue
} 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.
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.Status = IndexFailed
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.
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.Status = IndexFailed
return result

View file

@ -752,7 +752,17 @@ func (m *MediaFile) IsXMP() bool {
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 {
return m.MediaType() == fs.MediaSidecar
}

View file

@ -325,7 +325,15 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
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 {
t.Fatal(err)