mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Convert: Add --force flag to replace JPEGs in the sidecar folder #2214
This commit is contained in:
@@ -16,8 +16,14 @@ 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]",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "force, f",
|
||||||
|
Usage: "replace existing JPEG files in the sidecar folder",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: convertAction,
|
Action: convertAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
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
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user