Videos: Index and display durations of less than one second #3224
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
e7f6d79018
commit
eaff0abb6d
39 changed files with 827 additions and 379 deletions
|
@ -176,6 +176,8 @@ export default class Util {
|
|||
return "Bitmap";
|
||||
case "png":
|
||||
return "Portable Network Graphics";
|
||||
case "apng":
|
||||
return "Animated PNG";
|
||||
case "tiff":
|
||||
return "TIFF";
|
||||
case "psd":
|
||||
|
@ -189,6 +191,8 @@ export default class Util {
|
|||
return "Advanced Video Coding (AVC) / H.264";
|
||||
case "avif":
|
||||
return "AOMedia Video 1 (AV1)";
|
||||
case "avifs":
|
||||
return "AVIF Image Sequence";
|
||||
case "hevc":
|
||||
case "hvc":
|
||||
case "hvc1":
|
||||
|
@ -265,10 +269,14 @@ export default class Util {
|
|||
return "Motion JPEG (M-JPEG)";
|
||||
case "avif":
|
||||
return "AV1 Image File Format (AVIF)";
|
||||
case "avifs":
|
||||
return "AVIF Image Sequence";
|
||||
case "heif":
|
||||
return "High Efficiency Image File Format (HEIF)";
|
||||
case "heic":
|
||||
return "High Efficiency Image Container (HEIC)";
|
||||
case "heics":
|
||||
return "HEIC Image Sequence";
|
||||
case "1":
|
||||
return "Uncompressed";
|
||||
case "2":
|
||||
|
|
|
@ -200,10 +200,8 @@ export class File extends RestModel {
|
|||
isAnimated() {
|
||||
return (
|
||||
this.MediaType &&
|
||||
this.Frames &&
|
||||
this.MediaType === MediaImage &&
|
||||
this.Frames &&
|
||||
this.Frames > 1
|
||||
((this.Frames && this.Frames > 1) || (this.Duration && this.Duration > 1))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -481,7 +481,7 @@ export class Photo extends RestModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
return this.Files.find((f) => f.FileType === FormatGif || !!f.Frames);
|
||||
return this.Files.find((f) => f.FileType === FormatGif || !!f.Frames || !!f.Duration);
|
||||
}
|
||||
|
||||
videoUrl() {
|
||||
|
|
|
@ -82,7 +82,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
|||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else if !mimeType.Is(fs.MimeTypeJpeg) {
|
||||
} else if !mimeType.Is(fs.MimeTypeJPEG) {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "upload avatar", "only jpeg supported"}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUnsupportedFormat)
|
||||
return
|
||||
|
|
|
@ -52,14 +52,14 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
f, err = query.VideoByPhotoUID(f.PhotoUID)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("video: %s", err.Error())
|
||||
log.Errorf("video: no playable file found")
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.FileError != "" {
|
||||
log.Errorf("video: file error %s", f.FileError)
|
||||
log.Errorf("video: file has error %s", f.FileError)
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -618,7 +618,7 @@ var Flags = CliFlags{
|
|||
Flag: cli.StringFlag{
|
||||
Name: "imagemagick-blacklist",
|
||||
Usage: "do not use ImageMagick to convert files with these `EXTENSIONS`",
|
||||
Value: "",
|
||||
Value: "heif,heic,heics,avif,avifs,jxl",
|
||||
EnvVar: "PHOTOPRISM_IMAGEMAGICK_BLACKLIST",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
|
|
|
@ -655,7 +655,7 @@ func (m *File) SetDuration(d time.Duration) {
|
|||
return
|
||||
}
|
||||
|
||||
m.FileDuration = d.Round(time.Second)
|
||||
m.FileDuration = d.Round(10e6)
|
||||
|
||||
// Update number of frames.
|
||||
if m.FileFrames == 0 && m.FileFPS > 1 {
|
||||
|
|
|
@ -18,8 +18,8 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc
|
|||
// Don't transcode more than one video at the same time.
|
||||
useMutex = true
|
||||
|
||||
// Animated GIF or PNG?
|
||||
if fs.FileType(fileName) == fs.ImageGIF || fs.FileType(fileName) == fs.ImagePNG {
|
||||
// Don't use hardware transcoding for animated images.
|
||||
if fs.TypeAnimated[fs.FileType(fileName)] != "" {
|
||||
result = exec.Command(
|
||||
ffmpegBin,
|
||||
"-i", fileName,
|
||||
|
|
|
@ -23,10 +23,10 @@ type Data struct {
|
|||
TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"`
|
||||
TakenNs int `meta:"-"`
|
||||
TimeZone string `meta:"-"`
|
||||
Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration"`
|
||||
Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration,PreviewDuration"`
|
||||
FPS float64 `meta:"VideoFrameRate,VideoAvgFrameRate"`
|
||||
Frames int `meta:"FrameCount,AnimationFrames"`
|
||||
Codec string `meta:"CompressorID,VideoCodecID,CodecID,FileType"`
|
||||
Codec string `meta:"CompressorID,VideoCodecID,CodecID,OtherFormat,MajorBrand,FileType"`
|
||||
Title string `meta:"Headline,Title" xmp:"dc:title" dc:"title,title.Alt"`
|
||||
Subject string `meta:"Subject,PersonInImage,ObjectName,HierarchicalSubject,CatalogSets" xmp:"Subject"`
|
||||
Keywords Keywords `meta:"Keywords"`
|
||||
|
|
|
@ -1,40 +1,33 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
var DurationSecondsRegexp = regexp.MustCompile("[0-9\\.]+")
|
||||
|
||||
// StringToDuration converts a metadata string to a valid duration.
|
||||
func StringToDuration(s string) (d time.Duration) {
|
||||
// Duration converts a metadata string to a valid duration.
|
||||
func Duration(s string) (result time.Duration) {
|
||||
if s == "" {
|
||||
return d
|
||||
return 0
|
||||
}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
s = clean.Duration(s)
|
||||
|
||||
if s == "" {
|
||||
return d
|
||||
if txt.IsFloat(s) {
|
||||
result = time.Duration(txt.Float(s) * 1e9)
|
||||
} else if n := strings.Split(strings.TrimSpace(s), ":"); len(n) == 3 {
|
||||
hr, _ := strconv.Atoi(n[0])
|
||||
min, _ := strconv.Atoi(n[1])
|
||||
sec, _ := strconv.Atoi(n[2])
|
||||
|
||||
result = time.Duration(hr)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second
|
||||
} else if d, err := time.ParseDuration(s); err == nil {
|
||||
result = d
|
||||
}
|
||||
|
||||
sec := DurationSecondsRegexp.FindAllString(s, -1)
|
||||
|
||||
if len(sec) == 1 {
|
||||
secFloat, _ := strconv.ParseFloat(sec[0], 64)
|
||||
d = time.Duration(secFloat) * time.Second
|
||||
} else if n := strings.Split(s, ":"); len(n) == 3 {
|
||||
h, _ := strconv.Atoi(n[0])
|
||||
m, _ := strconv.Atoi(n[1])
|
||||
s, _ := strconv.Atoi(n[2])
|
||||
|
||||
d = time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second
|
||||
} else if pd, err := time.ParseDuration(s); err != nil {
|
||||
d = pd
|
||||
}
|
||||
|
||||
return d
|
||||
return result.Round(10e6)
|
||||
}
|
||||
|
|
|
@ -6,49 +6,54 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStringToDuration(t *testing.T) {
|
||||
func TestDuration(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
d := StringToDuration("")
|
||||
d := Duration("")
|
||||
assert.Equal(t, "0s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0", func(t *testing.T) {
|
||||
d := StringToDuration("0")
|
||||
d := Duration("0")
|
||||
assert.Equal(t, "0s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0.5", func(t *testing.T) {
|
||||
d := Duration("0.5")
|
||||
assert.Equal(t, "500ms", d.String())
|
||||
})
|
||||
|
||||
t.Run("2.41 s", func(t *testing.T) {
|
||||
d := StringToDuration("2.41 s")
|
||||
assert.Equal(t, "2s", d.String())
|
||||
d := Duration("2.41 s")
|
||||
assert.Equal(t, "2.41s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0.41 s", func(t *testing.T) {
|
||||
d := StringToDuration("0.41 s")
|
||||
assert.Equal(t, "0s", d.String())
|
||||
d := Duration("0.41 s")
|
||||
assert.Equal(t, "410ms", d.String())
|
||||
})
|
||||
|
||||
t.Run("41 s", func(t *testing.T) {
|
||||
d := StringToDuration("41 s")
|
||||
d := Duration("41 s")
|
||||
assert.Equal(t, "41s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0:0:1", func(t *testing.T) {
|
||||
d := StringToDuration("0:0:1")
|
||||
d := Duration("0:0:1")
|
||||
assert.Equal(t, "1s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0:04:25", func(t *testing.T) {
|
||||
d := StringToDuration("0:04:25")
|
||||
d := Duration("0:04:25")
|
||||
assert.Equal(t, "4m25s", d.String())
|
||||
})
|
||||
|
||||
t.Run("0001:04:25", func(t *testing.T) {
|
||||
d := StringToDuration("0001:04:25")
|
||||
d := Duration("0001:04:25")
|
||||
assert.Equal(t, "1h4m25s", d.String())
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
d := StringToDuration("01:04:25:67")
|
||||
d := Duration("01:04:25:67")
|
||||
assert.Equal(t, "0s", d.String())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func RawExif(fileName string, fileFormat fs.Type, bruteForce bool) (rawExif []by
|
|||
parsed = true
|
||||
}
|
||||
}
|
||||
case fs.ImageHEIC:
|
||||
case fs.ImageHEIF, fs.ImageHEIC, fs.ImageHEICS, fs.ImageAVIF, fs.ImageAVIFS:
|
||||
heicMp := heicexif.NewHeicExifMediaParser()
|
||||
|
||||
cs, err := heicMp.ParseFile(fileName)
|
||||
|
|
|
@ -100,7 +100,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
continue
|
||||
}
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(StringToDuration(jsonValue.String())))
|
||||
fieldValue.Set(reflect.ValueOf(Duration(jsonValue.String())))
|
||||
case int, int64:
|
||||
if !fieldValue.IsZero() {
|
||||
continue
|
||||
|
@ -118,7 +118,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
|
||||
if f := jsonValue.Float(); f != 0 {
|
||||
fieldValue.SetFloat(f)
|
||||
} else if f = txt.Float64(jsonValue.String()); f != 0 {
|
||||
} else if f = txt.Float(jsonValue.String()); f != 0 {
|
||||
fieldValue.SetFloat(f)
|
||||
}
|
||||
case uint, uint64:
|
||||
|
|
|
@ -23,7 +23,8 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "20170323-083538-Berlin-Zoologischer-Garten-2017-2u4.mov", data.FileName)
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, "3s", data.Duration.String())
|
||||
assert.Equal(t, int64(3540), data.Duration.Milliseconds())
|
||||
assert.Equal(t, "3.54s", data.Duration.String())
|
||||
assert.Equal(t, "2018-09-08 19:20:14 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2018-09-08 17:20:14 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
|
@ -49,7 +50,7 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "yoga-av1.webm", data.FileName)
|
||||
assert.Equal(t, "", data.Codec)
|
||||
assert.Equal(t, "20s", data.Duration.String())
|
||||
assert.Equal(t, "20.3s", data.Duration.String())
|
||||
assert.Equal(t, 854, data.Width)
|
||||
assert.Equal(t, 480, data.Height)
|
||||
assert.Equal(t, 854, data.ActualWidth())
|
||||
|
@ -65,7 +66,7 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "stream.webm", data.FileName)
|
||||
assert.Equal(t, CodecAv1, data.Codec)
|
||||
assert.Equal(t, "2m24s", data.Duration.String())
|
||||
assert.Equal(t, "2m24.12s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
assert.Equal(t, 1280, data.ActualWidth())
|
||||
|
@ -113,7 +114,7 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "earth-animation.ogv.720p.vp9.webm", data.FileName)
|
||||
assert.Equal(t, string(video.CodecVP9), data.Codec)
|
||||
assert.Equal(t, "8s", data.Duration.String())
|
||||
assert.Equal(t, "8.03s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
assert.Equal(t, 1280, data.ActualWidth())
|
||||
|
@ -130,7 +131,7 @@ func TestJSON(t *testing.T) {
|
|||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, "2s", data.Duration.String())
|
||||
assert.Equal(t, "2.41s", data.Duration.String())
|
||||
assert.Equal(t, "2020-05-11 14:18:35 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-05-11 14:18:35 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
|
@ -157,7 +158,7 @@ func TestJSON(t *testing.T) {
|
|||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, "2s", data.Duration.String())
|
||||
assert.Equal(t, "2.42s", data.Duration.String())
|
||||
assert.Equal(t, "2020-05-11 16:16:48 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-05-11 14:16:48 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
|
@ -184,7 +185,7 @@ func TestJSON(t *testing.T) {
|
|||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, "4s", data.Duration.String())
|
||||
assert.Equal(t, "4.3s", data.Duration.String())
|
||||
assert.Equal(t, "2020-05-14 13:34:41 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-05-14 11:34:41 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
|
@ -649,7 +650,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, "6s", data.Duration.String())
|
||||
assert.Equal(t, "6.83s", data.Duration.String())
|
||||
assert.Equal(t, "2015-06-10 14:06:09 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2015-06-10 11:06:09 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Moscow", data.TimeZone)
|
||||
|
@ -673,7 +674,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, "10s", data.Duration.String())
|
||||
assert.Equal(t, "10.67s", data.Duration.String())
|
||||
assert.Equal(t, "2015-12-06 18:22:29 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2015-12-06 15:22:29 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Moscow", data.TimeZone)
|
||||
|
@ -697,7 +698,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHEVC), data.Codec)
|
||||
assert.Equal(t, "6s", data.Duration.String())
|
||||
assert.Equal(t, "6.83s", data.Duration.String())
|
||||
assert.Equal(t, "2020-12-22 02:45:43 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-12-22 01:45:43 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "", data.TimeZone)
|
||||
|
@ -721,7 +722,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHEVC), data.Codec)
|
||||
assert.Equal(t, "2s", data.Duration.String())
|
||||
assert.Equal(t, "2.15s", data.Duration.String())
|
||||
assert.Equal(t, "2019-12-12 20:47:21 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2019-12-13 01:47:21 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "America/New_York", data.TimeZone)
|
||||
|
@ -758,7 +759,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, "6s", data.Duration.String())
|
||||
assert.Equal(t, "6.09s", data.Duration.String())
|
||||
assert.Equal(t, "2022-06-25 06:50:58 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2022-06-25 04:50:58 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "", data.TimeZone) // Local Time
|
||||
|
@ -934,7 +935,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, "1s", data.Duration.String())
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Paris", data.TimeZone)
|
||||
|
@ -951,7 +952,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, "1s", data.Duration.String())
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Paris", data.TimeZone)
|
||||
|
@ -968,7 +969,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, "1s", data.Duration.String())
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Paris", data.TimeZone)
|
||||
|
@ -1180,7 +1181,7 @@ func TestJSON(t *testing.T) {
|
|||
assert.Equal(t, 1533, data.Height)
|
||||
assert.Equal(t, 1917, data.Width)
|
||||
assert.Equal(t, 34, data.Frames)
|
||||
assert.Equal(t, 49*time.Second, data.Duration)
|
||||
assert.Equal(t, "49.5s", data.Duration.String())
|
||||
assert.Equal(t, float32(0), data.Lat)
|
||||
assert.Equal(t, float32(0), data.Lng)
|
||||
assert.Equal(t, 0.0, data.Altitude)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
|
@ -98,7 +99,7 @@ func (c *Convert) ToImage(f *MediaFile, force bool) (*MediaFile, error) {
|
|||
if err == nil {
|
||||
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(imageName)), time.Since(start), f.FileType())
|
||||
return NewMediaFile(imageName)
|
||||
} else if !f.IsTiff() {
|
||||
} else if !f.IsTIFF() {
|
||||
// See https://github.com/photoprism/photoprism/issues/1612
|
||||
// for TIFF file format compatibility.
|
||||
return nil, err
|
||||
|
@ -113,10 +114,10 @@ func (c *Convert) ToImage(f *MediaFile, force bool) (*MediaFile, error) {
|
|||
switch fs.LowerExt(imageName) {
|
||||
case fs.ExtPNG:
|
||||
cmds, useMutex, err = c.PngConvertCommands(f, imageName)
|
||||
expectedMime = fs.MimeTypePng
|
||||
expectedMime = fs.MimeTypePNG
|
||||
case fs.ExtJPEG:
|
||||
cmds, useMutex, err = c.JpegConvertCommands(f, imageName, xmpName)
|
||||
expectedMime = fs.MimeTypeJpeg
|
||||
expectedMime = fs.MimeTypeJPEG
|
||||
default:
|
||||
return nil, fmt.Errorf("convert: unspported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName()))
|
||||
}
|
||||
|
@ -157,11 +158,11 @@ func (c *Convert) ToImage(f *MediaFile, force bool) (*MediaFile, error) {
|
|||
|
||||
// Run convert command.
|
||||
if err = cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
err = errors.New(stderr.String())
|
||||
if errStr := strings.TrimSpace(stderr.String()); errStr != "" {
|
||||
err = errors.New(errStr)
|
||||
}
|
||||
|
||||
log.Tracef("convert: %s (%s)", err, filepath.Base(cmd.Path))
|
||||
log.Tracef("convert: %s (%s)", strings.TrimSpace(err.Error()), filepath.Base(cmd.Path))
|
||||
continue
|
||||
} else if fs.FileExistsNotEmpty(imageName) {
|
||||
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(imageName)), time.Since(start), filepath.Base(cmd.Path))
|
||||
|
|
|
@ -26,17 +26,17 @@ func (c *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||
result = append(result, exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName()))
|
||||
}
|
||||
|
||||
// Extract a still image to be used as preview.
|
||||
if f.IsAnimated() && c.conf.FFmpegEnabled() {
|
||||
// Use "ffmpeg" to extract a JPEG still image from the video.
|
||||
result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-vframes", "1", jpegName))
|
||||
}
|
||||
|
||||
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
||||
if (f.IsHEIC() || f.IsAVIF()) && c.conf.HeifConvertEnabled() {
|
||||
result = append(result, exec.Command(c.conf.HeifConvertBin(), "-q", c.conf.JpegQuality().String(), f.FileName(), jpegName))
|
||||
}
|
||||
|
||||
// Extract a video still image that can be used as preview.
|
||||
if f.IsVideo() && c.conf.FFmpegEnabled() {
|
||||
// Use "ffmpeg" to extract a JPEG still image from the video.
|
||||
result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-vframes", "1", jpegName))
|
||||
}
|
||||
|
||||
// RAW files may be concerted with Darktable and RawTherapee.
|
||||
if f.IsRaw() && c.conf.RawEnabled() {
|
||||
if c.conf.DarktableEnabled() && c.darktableBlacklist.Allow(fileExt) {
|
||||
|
@ -95,7 +95,7 @@ func (c *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||
|
||||
// Try ImageMagick for other image file formats if allowed.
|
||||
if c.conf.ImageMagickEnabled() && c.imagemagickBlacklist.Allow(fileExt) &&
|
||||
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() || f.IsVector() && c.conf.VectorEnabled()) {
|
||||
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() && !f.IsAnimated() || f.IsVector() && c.conf.VectorEnabled()) {
|
||||
quality := fmt.Sprintf("%d", c.conf.JpegQuality())
|
||||
resize := fmt.Sprintf("%dx%d>", c.conf.JpegSize(), c.conf.JpegSize())
|
||||
args := []string{f.FileName(), "-flatten", "-resize", resize, "-quality", quality, jpegName}
|
||||
|
|
|
@ -26,11 +26,16 @@ func (c *Convert) PngConvertCommands(f *MediaFile, pngName string) (result []*ex
|
|||
}
|
||||
|
||||
// Extract a video still image that can be used as preview.
|
||||
if f.IsVideo() && c.conf.FFmpegEnabled() {
|
||||
if f.IsAnimated() && c.conf.FFmpegEnabled() {
|
||||
// Use "ffmpeg" to extract a PNG still image from the video.
|
||||
result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-vframes", "1", pngName))
|
||||
}
|
||||
|
||||
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
||||
if (f.IsHEIC() || f.IsAVIF()) && c.conf.HeifConvertEnabled() {
|
||||
result = append(result, exec.Command(c.conf.HeifConvertBin(), f.FileName(), pngName))
|
||||
}
|
||||
|
||||
// Decode JPEG XL image if support is enabled.
|
||||
if f.IsJpegXL() && c.conf.JpegXLEnabled() {
|
||||
result = append(result, exec.Command(c.conf.JpegXLDecoderBin(), f.FileName(), pngName))
|
||||
|
@ -38,7 +43,7 @@ func (c *Convert) PngConvertCommands(f *MediaFile, pngName string) (result []*ex
|
|||
|
||||
// Try ImageMagick for other image file formats if allowed.
|
||||
if c.conf.ImageMagickEnabled() && c.imagemagickBlacklist.Allow(fileExt) &&
|
||||
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() || f.IsVector() && c.conf.VectorEnabled()) {
|
||||
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() && !f.IsAnimated() || f.IsVector() && c.conf.VectorEnabled()) {
|
||||
resize := fmt.Sprintf("%dx%d>", c.conf.PngSize(), c.conf.PngSize())
|
||||
args := []string{f.FileName(), "-flatten", "-resize", resize, pngName}
|
||||
result = append(result, exec.Command(c.conf.ImageMagickBin(), args...))
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -314,123 +313,6 @@ func (m *MediaFile) EditedName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// RelatedFiles returns files which are related to this file.
|
||||
func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) {
|
||||
// File path and name without any extensions.
|
||||
prefix := m.AbsPrefix(stripSequence)
|
||||
|
||||
// Storage folder path prefixes.
|
||||
sidecarPrefix := Config().SidecarPath() + "/"
|
||||
originalsPrefix := Config().OriginalsPath() + "/"
|
||||
|
||||
// Ignore RAW images?
|
||||
skipRaw := Config().DisableRaw()
|
||||
|
||||
// Ignore JPEG XL files?
|
||||
skipJpegXL := Config().DisableJpegXL()
|
||||
|
||||
// Ignore vector graphics?
|
||||
skipVectors := Config().DisableVectors()
|
||||
|
||||
// Replace sidecar with originals path in search prefix.
|
||||
if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) {
|
||||
prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1)
|
||||
log.Debugf("media: replaced sidecar with originals path in related file matching pattern")
|
||||
}
|
||||
|
||||
// Quote path for glob.
|
||||
if stripSequence {
|
||||
// Strip common name sequences like "copy 2" and escape meta characters.
|
||||
prefix = regexp.QuoteMeta(prefix)
|
||||
} else {
|
||||
// Use strict file name matching and escape meta characters.
|
||||
prefix = regexp.QuoteMeta(prefix + ".")
|
||||
}
|
||||
|
||||
// Find related files.
|
||||
matches, err := filepath.Glob(prefix + "*")
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if name := m.EditedName(); name != "" {
|
||||
matches = append(matches, name)
|
||||
}
|
||||
|
||||
isHEIC := false
|
||||
|
||||
for _, fileName := range matches {
|
||||
f, fileErr := NewMediaFile(fileName)
|
||||
|
||||
if fileErr != nil || f.Empty() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore file format?
|
||||
switch {
|
||||
case skipRaw && f.IsRaw():
|
||||
log.Debugf("media: skipped related raw image %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
case skipJpegXL && f.IsJpegXL():
|
||||
log.Debugf("media: skipped related JPEG XL file %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
case skipVectors && f.IsVector():
|
||||
log.Debugf("media: skipped related vector graphic %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Set main file.
|
||||
if result.Main == nil && f.IsPreviewImage() {
|
||||
result.Main = f
|
||||
} else if f.IsRaw() {
|
||||
result.Main = f
|
||||
} else if f.IsVector() {
|
||||
result.Main = f
|
||||
} else if f.IsHEIC() {
|
||||
isHEIC = true
|
||||
result.Main = f
|
||||
} else if f.IsImage() && !f.IsPreviewImage() {
|
||||
result.Main = f
|
||||
} else if f.IsVideo() && !isHEIC {
|
||||
result.Main = f
|
||||
} else if result.Main != nil && f.IsPreviewImage() {
|
||||
if result.Main.IsPreviewImage() && len(result.Main.FileName()) > len(f.FileName()) {
|
||||
result.Main = f
|
||||
}
|
||||
}
|
||||
|
||||
result.Files = append(result.Files, f)
|
||||
}
|
||||
|
||||
if len(result.Files) == 0 || result.Main == nil {
|
||||
t := m.MimeType()
|
||||
|
||||
if t == "" {
|
||||
t = "unknown type"
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("%s is unsupported (%s)", clean.Log(m.BaseName()), t)
|
||||
}
|
||||
|
||||
// Add hidden preview image if needed.
|
||||
if !result.HasPreview() {
|
||||
if jpegName := fs.ImageJPEG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
|
||||
if resultFile, _ := NewMediaFile(jpegName); resultFile.Ok() {
|
||||
result.Files = append(result.Files, resultFile)
|
||||
}
|
||||
} else if pngName := fs.ImagePNG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); pngName != "" {
|
||||
if resultFile, _ := NewMediaFile(pngName); resultFile.Ok() {
|
||||
result.Files = append(result.Files, resultFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(result.Files)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PathNameInfo returns file name infos for indexing.
|
||||
func (m *MediaFile) PathNameInfo(stripSequence bool) (fileRoot, fileBase, relativePath, relativeName string) {
|
||||
fileRoot = m.Root()
|
||||
|
@ -726,7 +608,7 @@ func (m *MediaFile) Extension() string {
|
|||
|
||||
// IsPreviewImage return true if this media file is a JPEG or PNG image.
|
||||
func (m *MediaFile) IsPreviewImage() bool {
|
||||
return m.IsJpeg() || m.IsPng()
|
||||
return m.IsJpeg() || m.IsPNG()
|
||||
}
|
||||
|
||||
// IsJpeg checks if the file is a JPEG image with a supported file type extension.
|
||||
|
@ -743,7 +625,7 @@ func (m *MediaFile) IsJpeg() bool {
|
|||
|
||||
// Since mime type detection is expensive, it is only
|
||||
// performed after other checks have passed.
|
||||
return m.MimeType() == fs.MimeTypeJpeg
|
||||
return m.MimeType() == fs.MimeTypeJPEG
|
||||
}
|
||||
|
||||
// IsJpegXL checks if the file is a JPEG XL image with a supported file type extension.
|
||||
|
@ -752,11 +634,11 @@ func (m *MediaFile) IsJpegXL() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeJpegXL
|
||||
return m.MimeType() == fs.MimeTypeJPEGXL
|
||||
}
|
||||
|
||||
// IsPng checks if the file is a PNG image with a supported file type extension.
|
||||
func (m *MediaFile) IsPng() bool {
|
||||
// IsPNG checks if the file is a PNG image with a supported file type extension.
|
||||
func (m *MediaFile) IsPNG() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImagePNG {
|
||||
// Files with an incorrect file extension are no longer
|
||||
// recognized as PNG to improve indexing performance.
|
||||
|
@ -766,25 +648,25 @@ func (m *MediaFile) IsPng() bool {
|
|||
// Since mime type detection is expensive, it is only
|
||||
// performed after other checks have passed.
|
||||
mimeType := m.MimeType()
|
||||
return mimeType == fs.MimeTypePng || mimeType == fs.MimeTypeAnimatedPng
|
||||
return mimeType == fs.MimeTypePNG || mimeType == fs.MimeTypeAPNG
|
||||
}
|
||||
|
||||
// IsGif checks if the file is a GIF image with a supported file type extension.
|
||||
func (m *MediaFile) IsGif() bool {
|
||||
// IsGIF checks if the file is a GIF image with a supported file type extension.
|
||||
func (m *MediaFile) IsGIF() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImageGIF {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeGif
|
||||
return m.MimeType() == fs.MimeTypeGIF
|
||||
}
|
||||
|
||||
// IsTiff checks if the file is a TIFF image with a supported file type extension.
|
||||
func (m *MediaFile) IsTiff() bool {
|
||||
// IsTIFF checks if the file is a TIFF image with a supported file type extension.
|
||||
func (m *MediaFile) IsTIFF() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImageTIFF {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeTiff
|
||||
return m.MimeType() == fs.MimeTypeTIFF
|
||||
}
|
||||
|
||||
// IsDNG checks if the file is a Adobe Digital Negative (DNG) image with a supported file type extension.
|
||||
|
@ -798,29 +680,39 @@ func (m *MediaFile) IsDNG() bool {
|
|||
|
||||
// IsHEIC checks if the file is a High Efficiency Image File Format (HEIC/HEIF) image with a supported file type extension.
|
||||
func (m *MediaFile) IsHEIC() bool {
|
||||
if t := fs.FileType(m.fileName); t != fs.ImageHEIC && t != fs.ImageHEIF {
|
||||
if t := fs.FileType(m.fileName); t != fs.ImageHEIF && t != fs.ImageHEIC {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeHEIC
|
||||
}
|
||||
|
||||
// IsHEICS checks if the file is a HEIC image sequence with a supported file type extension.
|
||||
func (m *MediaFile) IsHEICS() bool {
|
||||
return m.HasFileType(fs.ImageHEICS)
|
||||
}
|
||||
|
||||
// IsAVIF checks if the file is an AV1 Image File Format image with a supported file type extension.
|
||||
func (m *MediaFile) IsAVIF() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImageAVIF {
|
||||
if t := fs.FileType(m.fileName); t != fs.ImageAVIF {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeAVIF
|
||||
}
|
||||
|
||||
// IsBitmap checks if the file is a bitmap image with a supported file type extension.
|
||||
func (m *MediaFile) IsBitmap() bool {
|
||||
// IsAVIFS checks if the file is an AVIF image sequence with a supported file type extension.
|
||||
func (m *MediaFile) IsAVIFS() bool {
|
||||
return m.HasFileType(fs.ImageAVIFS)
|
||||
}
|
||||
|
||||
// IsBMP checks if the file is a bitmap image with a supported file type extension.
|
||||
func (m *MediaFile) IsBMP() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImageBMP {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeBitmap
|
||||
return m.MimeType() == fs.MimeTypeBMP
|
||||
}
|
||||
|
||||
// IsWebP checks if the file is a WebP image file with a supported file type extension.
|
||||
|
@ -841,13 +733,13 @@ func (m *MediaFile) Duration() time.Duration {
|
|||
return m.MetaData().Duration
|
||||
}
|
||||
|
||||
// IsAnimatedImage checks if the file is an animated image with a supported file type extension.
|
||||
// IsAnimatedImage checks if the file is an animated image.
|
||||
func (m *MediaFile) IsAnimatedImage() bool {
|
||||
return (m.IsGif() || m.IsPng()) && m.MetaData().Frames > 1
|
||||
return fs.FileAnimated(m.fileName) && (m.MetaData().Frames > 1 || m.MetaData().Duration > 0)
|
||||
}
|
||||
|
||||
// IsJson checks if the file is a JSON sidecar file with a supported file type extension.
|
||||
func (m *MediaFile) IsJson() bool {
|
||||
// IsJSON checks if the file is a JSON sidecar file with a supported file type extension.
|
||||
func (m *MediaFile) IsJSON() bool {
|
||||
return m.HasFileType(fs.SidecarJSON)
|
||||
}
|
||||
|
||||
|
@ -856,11 +748,11 @@ func (m *MediaFile) FileType() fs.Type {
|
|||
switch {
|
||||
case m.IsJpeg():
|
||||
return fs.ImageJPEG
|
||||
case m.IsPng():
|
||||
case m.IsPNG():
|
||||
return fs.ImagePNG
|
||||
case m.IsGif():
|
||||
case m.IsGIF():
|
||||
return fs.ImageGIF
|
||||
case m.IsBitmap():
|
||||
case m.IsBMP():
|
||||
return fs.ImageBMP
|
||||
case m.IsDNG():
|
||||
return fs.ImageDNG
|
||||
|
@ -950,7 +842,7 @@ func (m *MediaFile) IsPlayableVideo() bool {
|
|||
// IsImageOther returns true if this is a PNG, GIF, BMP, TIFF, or WebP file.
|
||||
func (m *MediaFile) IsImageOther() bool {
|
||||
switch {
|
||||
case m.IsPng(), m.IsGif(), m.IsTiff(), m.IsBitmap(), m.IsWebP():
|
||||
case m.IsPNG(), m.IsGIF(), m.IsTIFF(), m.IsBMP(), m.IsWebP():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -977,7 +869,7 @@ func (m *MediaFile) IsLive() bool {
|
|||
|
||||
// ExifSupported returns true if parsing exif metadata is supported for the media file type.
|
||||
func (m *MediaFile) ExifSupported() bool {
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHEIC() || m.IsPng() || m.IsTiff()
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHEIC() || m.IsHEICS() || m.IsAVIF() || m.IsAVIFS() || m.IsPNG() || m.IsTIFF()
|
||||
}
|
||||
|
||||
// IsMedia returns true if this is a media file (photo or video, not sidecar or other).
|
||||
|
@ -1025,13 +917,13 @@ func (m *MediaFile) HasPreviewImage() bool {
|
|||
|
||||
jpegName := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if m.hasPreviewImage = fs.MimeType(jpegName) == fs.MimeTypeJpeg; m.hasPreviewImage {
|
||||
if m.hasPreviewImage = fs.MimeType(jpegName) == fs.MimeTypeJPEG; m.hasPreviewImage {
|
||||
return true
|
||||
}
|
||||
|
||||
pngName := fs.ImagePNG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if m.hasPreviewImage = fs.MimeType(pngName) == fs.MimeTypePng; m.hasPreviewImage {
|
||||
if m.hasPreviewImage = fs.MimeType(pngName) == fs.MimeTypePNG; m.hasPreviewImage {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// HasSidecarJson returns true if this file has or is a json sidecar file.
|
||||
func (m *MediaFile) HasSidecarJson() bool {
|
||||
if m.IsJson() {
|
||||
if m.IsJSON() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
131
internal/photoprism/mediafile_related.go
Normal file
131
internal/photoprism/mediafile_related.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// RelatedFiles returns files which are related to this file.
|
||||
func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) {
|
||||
// File path and name without any extensions.
|
||||
prefix := m.AbsPrefix(stripSequence)
|
||||
|
||||
// Storage folder path prefixes.
|
||||
sidecarPrefix := Config().SidecarPath() + "/"
|
||||
originalsPrefix := Config().OriginalsPath() + "/"
|
||||
|
||||
// Ignore RAW images?
|
||||
skipRaw := Config().DisableRaw()
|
||||
|
||||
// Ignore JPEG XL files?
|
||||
skipJpegXL := Config().DisableJpegXL()
|
||||
|
||||
// Ignore vector graphics?
|
||||
skipVectors := Config().DisableVectors()
|
||||
|
||||
// Replace sidecar with originals path in search prefix.
|
||||
if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) {
|
||||
prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1)
|
||||
log.Debugf("media: replaced sidecar with originals path in related file matching pattern")
|
||||
}
|
||||
|
||||
// Quote path for glob.
|
||||
if stripSequence {
|
||||
// Strip common name sequences like "copy 2" and escape meta characters.
|
||||
prefix = regexp.QuoteMeta(prefix)
|
||||
} else {
|
||||
// Use strict file name matching and escape meta characters.
|
||||
prefix = regexp.QuoteMeta(prefix + ".")
|
||||
}
|
||||
|
||||
// Find related files.
|
||||
matches, err := filepath.Glob(prefix + "*")
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if name := m.EditedName(); name != "" {
|
||||
matches = append(matches, name)
|
||||
}
|
||||
|
||||
isHEIC := false
|
||||
|
||||
for _, fileName := range matches {
|
||||
f, fileErr := NewMediaFile(fileName)
|
||||
|
||||
if fileErr != nil || f.Empty() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore file format?
|
||||
switch {
|
||||
case skipRaw && f.IsRaw():
|
||||
log.Debugf("media: skipped related raw image %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
case skipJpegXL && f.IsJpegXL():
|
||||
log.Debugf("media: skipped related JPEG XL file %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
case skipVectors && f.IsVector():
|
||||
log.Debugf("media: skipped related vector graphic %s", clean.Log(f.RootRelName()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Set main file.
|
||||
if result.Main == nil && f.IsPreviewImage() {
|
||||
result.Main = f
|
||||
} else if f.IsRaw() {
|
||||
result.Main = f
|
||||
} else if f.IsVector() {
|
||||
result.Main = f
|
||||
} else if f.IsHEIC() {
|
||||
isHEIC = true
|
||||
result.Main = f
|
||||
} else if f.IsAVIF() {
|
||||
result.Main = f
|
||||
} else if f.IsImage() && !f.IsPreviewImage() {
|
||||
result.Main = f
|
||||
} else if f.IsVideo() && !isHEIC {
|
||||
result.Main = f
|
||||
} else if result.Main != nil && f.IsPreviewImage() {
|
||||
if result.Main.IsPreviewImage() && len(result.Main.FileName()) > len(f.FileName()) {
|
||||
result.Main = f
|
||||
}
|
||||
}
|
||||
|
||||
result.Files = append(result.Files, f)
|
||||
}
|
||||
|
||||
if len(result.Files) == 0 || result.Main == nil {
|
||||
t := m.MimeType()
|
||||
|
||||
if t == "" {
|
||||
t = "unknown type"
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("%s is unsupported (%s)", clean.Log(m.BaseName()), t)
|
||||
}
|
||||
|
||||
// Add hidden preview image if needed.
|
||||
if !result.HasPreview() {
|
||||
if jpegName := fs.ImageJPEG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
|
||||
if resultFile, _ := NewMediaFile(jpegName); resultFile.Ok() {
|
||||
result.Files = append(result.Files, resultFile)
|
||||
}
|
||||
} else if pngName := fs.ImagePNG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); pngName != "" {
|
||||
if resultFile, _ := NewMediaFile(pngName); resultFile.Ok() {
|
||||
result.Files = append(result.Files, resultFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(result.Files)
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -6,7 +6,6 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
|
@ -1216,7 +1215,7 @@ func TestMediaFile_IsPng(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, false, mediaFile.IsPng())
|
||||
assert.Equal(t, false, mediaFile.IsPNG())
|
||||
})
|
||||
t.Run("tweethog.png", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png")
|
||||
|
@ -1227,7 +1226,7 @@ func TestMediaFile_IsPng(t *testing.T) {
|
|||
|
||||
assert.Equal(t, fs.ImagePNG, mediaFile.FileType())
|
||||
assert.Equal(t, "image/png", mediaFile.MimeType())
|
||||
assert.Equal(t, true, mediaFile.IsPng())
|
||||
assert.Equal(t, true, mediaFile.IsPNG())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1241,7 +1240,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, fs.SidecarJSON, mediaFile.FileType())
|
||||
assert.Equal(t, fs.MimeTypeJSON, mediaFile.MimeType())
|
||||
assert.Equal(t, false, mediaFile.IsTiff())
|
||||
assert.Equal(t, false, mediaFile.IsTIFF())
|
||||
})
|
||||
t.Run("purple.tiff", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/purple.tiff")
|
||||
|
@ -1250,7 +1249,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, fs.ImageTIFF, mediaFile.FileType())
|
||||
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
||||
assert.Equal(t, true, mediaFile.IsTiff())
|
||||
assert.Equal(t, true, mediaFile.IsTIFF())
|
||||
})
|
||||
t.Run("example.tiff", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/example.tif")
|
||||
|
@ -1259,7 +1258,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, fs.ImageTIFF, mediaFile.FileType())
|
||||
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
||||
assert.Equal(t, true, mediaFile.IsTiff())
|
||||
assert.Equal(t, true, mediaFile.IsTIFF())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1286,9 +1285,9 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, false, mediaFile.IsJpeg())
|
||||
assert.Equal(t, false, mediaFile.IsGif())
|
||||
assert.Equal(t, true, mediaFile.IsPng())
|
||||
assert.Equal(t, false, mediaFile.IsBitmap())
|
||||
assert.Equal(t, false, mediaFile.IsGIF())
|
||||
assert.Equal(t, true, mediaFile.IsPNG())
|
||||
assert.Equal(t, false, mediaFile.IsBMP())
|
||||
assert.Equal(t, false, mediaFile.IsWebP())
|
||||
assert.Equal(t, true, mediaFile.IsImage())
|
||||
assert.Equal(t, true, mediaFile.IsImageNative())
|
||||
|
@ -1304,8 +1303,8 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
|||
assert.Equal(t, fs.ImageBMP, mediaFile.FileType())
|
||||
assert.Equal(t, "image/bmp", mediaFile.MimeType())
|
||||
assert.Equal(t, false, mediaFile.IsJpeg())
|
||||
assert.Equal(t, false, mediaFile.IsGif())
|
||||
assert.Equal(t, true, mediaFile.IsBitmap())
|
||||
assert.Equal(t, false, mediaFile.IsGIF())
|
||||
assert.Equal(t, true, mediaFile.IsBMP())
|
||||
assert.Equal(t, false, mediaFile.IsWebP())
|
||||
assert.Equal(t, true, mediaFile.IsImage())
|
||||
assert.Equal(t, true, mediaFile.IsImageNative())
|
||||
|
@ -1322,8 +1321,8 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
|||
assert.Equal(t, fs.ImageGIF, mediaFile.FileType())
|
||||
assert.Equal(t, "image/gif", mediaFile.MimeType())
|
||||
assert.Equal(t, false, mediaFile.IsJpeg())
|
||||
assert.Equal(t, true, mediaFile.IsGif())
|
||||
assert.Equal(t, false, mediaFile.IsBitmap())
|
||||
assert.Equal(t, true, mediaFile.IsGIF())
|
||||
assert.Equal(t, false, mediaFile.IsBMP())
|
||||
assert.Equal(t, false, mediaFile.IsWebP())
|
||||
assert.Equal(t, true, mediaFile.IsImage())
|
||||
assert.Equal(t, true, mediaFile.IsImageNative())
|
||||
|
@ -1341,8 +1340,8 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
|||
assert.Equal(t, fs.ImageWebP, mediaFile.FileType())
|
||||
assert.Equal(t, fs.MimeTypeWebP, mediaFile.MimeType())
|
||||
assert.Equal(t, false, mediaFile.IsJpeg())
|
||||
assert.Equal(t, false, mediaFile.IsGif())
|
||||
assert.Equal(t, false, mediaFile.IsBitmap())
|
||||
assert.Equal(t, false, mediaFile.IsGIF())
|
||||
assert.Equal(t, false, mediaFile.IsBMP())
|
||||
assert.Equal(t, true, mediaFile.IsWebP())
|
||||
assert.Equal(t, true, mediaFile.IsImage())
|
||||
assert.Equal(t, true, mediaFile.IsImageNative())
|
||||
|
@ -1467,7 +1466,7 @@ func TestMediaFile_IsVideo(t *testing.T) {
|
|||
assert.Equal(t, false, f.IsRaw())
|
||||
assert.Equal(t, false, f.IsImage())
|
||||
assert.Equal(t, true, f.IsVideo())
|
||||
assert.Equal(t, false, f.IsJson())
|
||||
assert.Equal(t, false, f.IsJSON())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
})
|
||||
|
@ -1478,7 +1477,7 @@ func TestMediaFile_IsVideo(t *testing.T) {
|
|||
assert.Equal(t, true, f.IsRaw())
|
||||
assert.Equal(t, false, f.IsImage())
|
||||
assert.Equal(t, false, f.IsVideo())
|
||||
assert.Equal(t, false, f.IsJson())
|
||||
assert.Equal(t, false, f.IsJSON())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
})
|
||||
|
@ -1489,7 +1488,7 @@ func TestMediaFile_IsVideo(t *testing.T) {
|
|||
assert.Equal(t, false, f.IsRaw())
|
||||
assert.Equal(t, false, f.IsImage())
|
||||
assert.Equal(t, false, f.IsVideo())
|
||||
assert.Equal(t, true, f.IsJson())
|
||||
assert.Equal(t, true, f.IsJSON())
|
||||
assert.Equal(t, true, f.IsSidecar())
|
||||
}
|
||||
})
|
||||
|
@ -1497,7 +1496,23 @@ func TestMediaFile_IsVideo(t *testing.T) {
|
|||
|
||||
func TestMediaFile_IsAnimated(t *testing.T) {
|
||||
cnf := config.TestConfig()
|
||||
|
||||
t.Run("star.avifs", func(t *testing.T) {
|
||||
if f, err := NewMediaFile("testdata/star.avifs"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, true, f.IsImage())
|
||||
assert.Equal(t, true, f.IsAVIFS())
|
||||
assert.Equal(t, true, f.IsAnimated())
|
||||
assert.Equal(t, true, f.IsAnimatedImage())
|
||||
assert.Equal(t, true, f.ExifSupported())
|
||||
assert.Equal(t, false, f.IsVideo())
|
||||
assert.Equal(t, false, f.IsGIF())
|
||||
assert.Equal(t, false, f.IsAVIF())
|
||||
assert.Equal(t, false, f.IsHEIC())
|
||||
assert.Equal(t, false, f.IsHEICS())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
})
|
||||
t.Run("example.gif", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "example.gif")); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1505,7 +1520,7 @@ func TestMediaFile_IsAnimated(t *testing.T) {
|
|||
assert.Equal(t, true, f.IsImage())
|
||||
assert.Equal(t, false, f.IsVideo())
|
||||
assert.Equal(t, false, f.IsAnimated())
|
||||
assert.Equal(t, true, f.IsGif())
|
||||
assert.Equal(t, true, f.IsGIF())
|
||||
assert.Equal(t, false, f.IsAnimatedImage())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
|
@ -1517,7 +1532,7 @@ func TestMediaFile_IsAnimated(t *testing.T) {
|
|||
assert.Equal(t, true, f.IsImage())
|
||||
assert.Equal(t, false, f.IsVideo())
|
||||
assert.Equal(t, true, f.IsAnimated())
|
||||
assert.Equal(t, true, f.IsGif())
|
||||
assert.Equal(t, true, f.IsGIF())
|
||||
assert.Equal(t, true, f.IsAnimatedImage())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
|
@ -1529,7 +1544,7 @@ func TestMediaFile_IsAnimated(t *testing.T) {
|
|||
assert.Equal(t, false, f.IsImage())
|
||||
assert.Equal(t, true, f.IsVideo())
|
||||
assert.Equal(t, true, f.IsAnimated())
|
||||
assert.Equal(t, false, f.IsGif())
|
||||
assert.Equal(t, false, f.IsGIF())
|
||||
assert.Equal(t, false, f.IsAnimatedImage())
|
||||
assert.Equal(t, false, f.IsSidecar())
|
||||
}
|
||||
|
@ -2141,7 +2156,7 @@ func TestMediaFile_FileType(t *testing.T) {
|
|||
|
||||
// No longer recognized as JPEG to improve indexing performance (skips mime type detection).
|
||||
assert.False(t, m.IsJpeg())
|
||||
assert.False(t, m.IsPng())
|
||||
assert.False(t, m.IsPNG())
|
||||
assert.Equal(t, "png", string(m.FileType()))
|
||||
assert.Equal(t, "image/jpeg", m.MimeType())
|
||||
assert.Equal(t, fs.ImagePNG, m.FileType())
|
||||
|
@ -2328,7 +2343,7 @@ func TestMediaFile_IsJson(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.False(t, mediaFile.IsJson())
|
||||
assert.False(t, mediaFile.IsJSON())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -2339,7 +2354,7 @@ func TestMediaFile_IsJson(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, mediaFile.IsJson())
|
||||
assert.True(t, mediaFile.IsJSON())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2511,7 +2526,7 @@ func TestMediaFile_Duration(t *testing.T) {
|
|||
if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "blue-go-video.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, time.Duration(2000000000), f.Duration())
|
||||
assert.Equal(t, "2.42s", f.Duration().String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
BIN
internal/photoprism/testdata/star.avifs
vendored
Normal file
BIN
internal/photoprism/testdata/star.avifs
vendored
Normal file
Binary file not shown.
64
internal/photoprism/testdata/star.avifs.json
vendored
Normal file
64
internal/photoprism/testdata/star.avifs.json
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
[{
|
||||
"SourceFile": "testdata/star.avifs",
|
||||
"ExifToolVersion": 12.40,
|
||||
"FileName": "star.avifs",
|
||||
"Directory": "testdata",
|
||||
"FileSize": 15679,
|
||||
"FileModifyDate": "2023:02:22 10:07:46+00:00",
|
||||
"FileAccessDate": "2023:02:22 10:09:03+00:00",
|
||||
"FileInodeChangeDate": "2023:02:22 10:09:02+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "avis",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["avis","msf1","miaf","MA1B"],
|
||||
"PrimaryItemReference": 1,
|
||||
"ImageWidth": 159,
|
||||
"ImageHeight": 159,
|
||||
"ImageSpatialExtent": "159 159",
|
||||
"PixelAspectRatio": "1 1",
|
||||
"AV1ConfigurationVersion": 1,
|
||||
"ChromaFormat": 3,
|
||||
"ChromaSamplePosition": 0,
|
||||
"ImagePixelDepth": "8 8 8",
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2020:04:07 16:39:21",
|
||||
"ModifyDate": "2020:04:07 16:39:21",
|
||||
"TimeScale": 1000,
|
||||
"Duration": 0.5,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 2,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "0000:00:00 00:00:00",
|
||||
"TrackModifyDate": "2020:04:07 16:39:21",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 0.5,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "0000:00:00 00:00:00",
|
||||
"MediaModifyDate": "2020:04:07 16:39:21",
|
||||
"MediaTimeScale": 10240,
|
||||
"MediaDuration": 0.5,
|
||||
"MediaLanguageCode": "und",
|
||||
"HandlerType": "pict",
|
||||
"HandlerDescription": "GPAC avifs",
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"OtherFormat": "av01",
|
||||
"MediaDataSize": 14556,
|
||||
"MediaDataOffset": 1051,
|
||||
"ImageSize": "159 159",
|
||||
"Megapixels": 0.025281,
|
||||
"AvgBitrate": 232896
|
||||
}]
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
|
@ -71,6 +72,7 @@ func FileByPhotoUID(photoUID string) (*entity.File, error) {
|
|||
}
|
||||
|
||||
err := Db().Where("photo_uid = ? AND file_primary = 1", photoUID).Preload("Photo").First(&f).Error
|
||||
|
||||
return &f, err
|
||||
}
|
||||
|
||||
|
@ -82,9 +84,11 @@ func VideoByPhotoUID(photoUID string) (*entity.File, error) {
|
|||
return &f, fmt.Errorf("photo uid required")
|
||||
}
|
||||
|
||||
err := Db().Where("photo_uid = ? AND (file_video = 1 OR file_frames > 0 OR file_type = 'gif')", photoUID).
|
||||
Order("file_video DESC, file_duration DESC, file_frames DESC").
|
||||
err := Db().Where("photo_uid = ? AND file_missing = 0", photoUID).
|
||||
Where("file_video = 1 OR file_duration > 0 OR file_frames > 0 OR file_type = ?", fs.ImageGIF).
|
||||
Order("file_error ASC, file_video DESC, file_duration DESC, file_frames DESC").
|
||||
Preload("Photo").First(&f).Error
|
||||
|
||||
return &f, err
|
||||
}
|
||||
|
||||
|
@ -97,6 +101,7 @@ func FileByUID(fileUID string) (*entity.File, error) {
|
|||
}
|
||||
|
||||
err := Db().Where("file_uid = ?", fileUID).Preload("Photo").First(&f).Error
|
||||
|
||||
return &f, err
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ var PhotosColsAll = SelectString(Photo{}, []string{"*"})
|
|||
var PhotosColsView = SelectString(Photo{}, SelectCols(GeoResult{}, []string{"*"}))
|
||||
|
||||
// FileTypes contains a list of browser-compatible file formats returned by search queries.
|
||||
var FileTypes = []string{fs.ImageJPEG.String(), fs.ImagePNG.String(), fs.ImageGIF.String(), fs.ImageWebP.String(), fs.VectorSVG.String()}
|
||||
var FileTypes = []string{fs.ImageJPEG.String(), fs.ImagePNG.String(), fs.ImageGIF.String(), fs.ImageAVIF.String(), fs.ImageAVIFS.String(), fs.ImageWebP.String(), fs.VectorSVG.String()}
|
||||
|
||||
// Photos finds PhotoResults based on the search form without checking rights or permissions.
|
||||
func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||
|
|
47
pkg/clean/duration.go
Normal file
47
pkg/clean/duration.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package clean
|
||||
|
||||
import "strings"
|
||||
|
||||
var durationRunes = map[rune]bool{
|
||||
':': true,
|
||||
'-': true,
|
||||
'd': true,
|
||||
'h': true,
|
||||
'm': true,
|
||||
's': true,
|
||||
'n': true,
|
||||
'µ': true,
|
||||
}
|
||||
|
||||
// Duration removes non-duration characters from a string and returns the result.
|
||||
func Duration(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
valid := false
|
||||
skipDot := false
|
||||
|
||||
// Remove invalid characters.
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if !skipDot && (r == ',' || r == '.') {
|
||||
skipDot = true
|
||||
return '.'
|
||||
} else if durationRunes[r] {
|
||||
skipDot = false
|
||||
return r
|
||||
} else if r < '0' || r > '9' {
|
||||
return -1
|
||||
}
|
||||
|
||||
valid = true
|
||||
|
||||
return r
|
||||
}, s)
|
||||
|
||||
if !valid {
|
||||
return ""
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
69
pkg/clean/duration_test.go
Normal file
69
pkg/clean/duration_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := Duration("")
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("NonNumeric", func(t *testing.T) {
|
||||
result := Duration(" Screenshot ")
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
result := Duration("0")
|
||||
assert.Equal(t, "0", result)
|
||||
})
|
||||
|
||||
t.Run("Float", func(t *testing.T) {
|
||||
result := Duration("0.5")
|
||||
assert.Equal(t, "0.5", result)
|
||||
})
|
||||
|
||||
t.Run("Seconds", func(t *testing.T) {
|
||||
result := Duration("0.5 s")
|
||||
assert.Equal(t, "0.5s", result)
|
||||
})
|
||||
|
||||
t.Run("MinutesSeconds", func(t *testing.T) {
|
||||
result := Duration("1.0 m0.01 s ")
|
||||
assert.Equal(t, "1.0m0.01s", result)
|
||||
})
|
||||
|
||||
t.Run("01:00", func(t *testing.T) {
|
||||
result := Duration("01:00")
|
||||
assert.Equal(t, "01:00", result)
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
result := Duration(" 000123")
|
||||
assert.Equal(t, "000123", result)
|
||||
})
|
||||
|
||||
t.Run("WhitespacePadding", func(t *testing.T) {
|
||||
result := Duration(" 123,556\t ")
|
||||
assert.Equal(t, "123.556", result)
|
||||
})
|
||||
|
||||
t.Run("PositiveFloat", func(t *testing.T) {
|
||||
result := Duration("123,000.45245 ")
|
||||
assert.Equal(t, "123.00045245", result)
|
||||
})
|
||||
|
||||
t.Run("NegativeFloat", func(t *testing.T) {
|
||||
result := Duration(" - 123,000.45245 ")
|
||||
assert.Equal(t, "-123.00045245", result)
|
||||
})
|
||||
|
||||
t.Run("MultipleDots", func(t *testing.T) {
|
||||
result := Duration("123.000.45245.44 m")
|
||||
assert.Equal(t, "123.0004524544m", result)
|
||||
})
|
||||
}
|
10
pkg/clean/numeric.go
Normal file
10
pkg/clean/numeric.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Numeric removes non-numeric characters from a string and returns the result.
|
||||
func Numeric(s string) string {
|
||||
return txt.Numeric(s)
|
||||
}
|
59
pkg/clean/numeric_test.go
Normal file
59
pkg/clean/numeric_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNumeric(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := Numeric("")
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("NonNumeric", func(t *testing.T) {
|
||||
result := Numeric(" Screenshot ")
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
result := Numeric("0")
|
||||
assert.Equal(t, "0", result)
|
||||
})
|
||||
|
||||
t.Run("0.5", func(t *testing.T) {
|
||||
result := Numeric("0.5")
|
||||
assert.Equal(t, "0.5", result)
|
||||
})
|
||||
|
||||
t.Run("01:00", func(t *testing.T) {
|
||||
result := Numeric("01:00")
|
||||
assert.Equal(t, "0100", result)
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
result := Numeric(" 000123")
|
||||
assert.Equal(t, "000123", result)
|
||||
})
|
||||
|
||||
t.Run("WhitespacePadding", func(t *testing.T) {
|
||||
result := Numeric(" 123,556\t ")
|
||||
assert.Equal(t, "123.556", result)
|
||||
})
|
||||
|
||||
t.Run("PositiveFloat", func(t *testing.T) {
|
||||
result := Numeric("123,000.45245 ")
|
||||
assert.Equal(t, "123000.45245", result)
|
||||
})
|
||||
|
||||
t.Run("NegativeFloat", func(t *testing.T) {
|
||||
result := Numeric(" - 123,000.45245 ")
|
||||
assert.Equal(t, "-123000.45245", result)
|
||||
})
|
||||
|
||||
t.Run("MultipleDots", func(t *testing.T) {
|
||||
result := Numeric("123.000.45245.44 m")
|
||||
assert.Equal(t, "1230004524544", result)
|
||||
})
|
||||
}
|
|
@ -23,19 +23,21 @@ var Extensions = FileExtensions{
|
|||
".psd": ImagePSD,
|
||||
".png": ImagePNG,
|
||||
".apng": ImagePNG,
|
||||
".pnga": ImagePNG,
|
||||
".pn": ImagePNG,
|
||||
".gif": ImageGIF,
|
||||
".bmp": ImageBMP,
|
||||
".dng": ImageDNG,
|
||||
".avif": ImageAVIF,
|
||||
".avifs": ImageAVIF,
|
||||
".heif": ImageHEIC,
|
||||
".avis": ImageAVIFS,
|
||||
".avifs": ImageAVIFS,
|
||||
".hif": ImageHEIC,
|
||||
".heif": ImageHEIC,
|
||||
".heic": ImageHEIC,
|
||||
".heifs": ImageHEIC,
|
||||
".heics": ImageHEIC,
|
||||
".avci": ImageHEIC,
|
||||
".avcs": ImageHEIC,
|
||||
".heifs": ImageHEICS,
|
||||
".heics": ImageHEICS,
|
||||
".webp": ImageWebP,
|
||||
".mpo": ImageMPO,
|
||||
".3fr": ImageRaw,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package fs
|
||||
|
||||
type TypeMap map[Type]string
|
||||
|
||||
// TypeInfo contains human-readable descriptions for supported file formats
|
||||
var TypeInfo = map[Type]string{
|
||||
var TypeInfo = TypeMap{
|
||||
ImageRaw: "Unprocessed Sensor Data",
|
||||
ImageDNG: "Adobe Digital Negative",
|
||||
ImageJPEG: "Joint Photographic Experts Group (JPEG)",
|
||||
|
@ -13,8 +15,10 @@ var TypeInfo = map[Type]string{
|
|||
ImageBMP: "Bitmap",
|
||||
ImageMPO: "Stereoscopic JPEG (3D)",
|
||||
ImageAVIF: "AV1 Image File Format",
|
||||
ImageAVIFS: "AV1 Image Sequence",
|
||||
ImageHEIF: "High Efficiency Image File Format",
|
||||
ImageHEIC: "High Efficiency Image Container",
|
||||
ImageHEICS: "HEIC Image Sequence",
|
||||
ImageWebP: "Google WebP",
|
||||
VideoWebM: "Google WebM",
|
||||
VideoMP2: "MPEG 2 (H.262)",
|
|
@ -17,6 +17,15 @@ func FileType(fileName string) Type {
|
|||
return UnknownType
|
||||
}
|
||||
|
||||
// FileAnimated checks if the type associated with the specified filename may be animated.
|
||||
func FileAnimated(fileName string) bool {
|
||||
if t, found := Extensions[LowerExt(fileName)]; found {
|
||||
return TypeAnimated[t] != ""
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NewType creates a new file type from a filename extension.
|
||||
func NewType(ext string) Type {
|
||||
return Type(TrimExt(ext))
|
||||
|
|
|
@ -148,7 +148,7 @@ func TestType_FindAll(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
func TestFileType(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := FileType("")
|
||||
assert.Equal(t, UnknownType, result)
|
||||
|
@ -157,7 +157,7 @@ func TestType(t *testing.T) {
|
|||
result := FileType("testdata/test.jpg")
|
||||
assert.Equal(t, ImageJPEG, result)
|
||||
})
|
||||
t.Run("RawCRw", func(t *testing.T) {
|
||||
t.Run("RawCRW", func(t *testing.T) {
|
||||
result := FileType("testdata/test (jpg).crw")
|
||||
assert.Equal(t, ImageRaw, result)
|
||||
})
|
||||
|
@ -169,3 +169,36 @@ func TestType(t *testing.T) {
|
|||
assert.Equal(t, Type("mp4"), FileType("file.mp"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileAnimated(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, FileAnimated(""))
|
||||
})
|
||||
t.Run("JPEG", func(t *testing.T) {
|
||||
assert.False(t, FileAnimated("testdata/test.jpg"))
|
||||
})
|
||||
t.Run("RawCRW", func(t *testing.T) {
|
||||
assert.False(t, FileAnimated("testdata/test (jpg).crw"))
|
||||
})
|
||||
t.Run("MP4", func(t *testing.T) {
|
||||
assert.False(t, FileAnimated("file.mp"))
|
||||
assert.False(t, FileAnimated("file.mp4"))
|
||||
})
|
||||
t.Run("GIF", func(t *testing.T) {
|
||||
assert.True(t, FileAnimated("file.gif"))
|
||||
})
|
||||
t.Run("PNG", func(t *testing.T) {
|
||||
assert.True(t, FileAnimated("file.png"))
|
||||
assert.True(t, FileAnimated("file.apng"))
|
||||
assert.True(t, FileAnimated("file.pnga"))
|
||||
})
|
||||
t.Run("AVIF", func(t *testing.T) {
|
||||
assert.True(t, FileAnimated("file.avif"))
|
||||
assert.True(t, FileAnimated("file.avis"))
|
||||
assert.True(t, FileAnimated("file.avifs"))
|
||||
})
|
||||
t.Run("HEIC", func(t *testing.T) {
|
||||
assert.True(t, FileAnimated("file.heic"))
|
||||
assert.True(t, FileAnimated("file.heics"))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,52 +10,64 @@ import (
|
|||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// File types.
|
||||
// Supported file types.
|
||||
const (
|
||||
ImageRaw Type = "raw" // RAW image
|
||||
ImageJPEG Type = "jpg" // JPEG image
|
||||
ImageJPEGXL Type = "jxl" // JPEG XL image
|
||||
ImagePNG Type = "png" // PNG image
|
||||
ImageGIF Type = "gif" // GIF image
|
||||
ImageTIFF Type = "tiff" // TIFF image
|
||||
ImagePSD Type = "psd" // Adobe Photoshop
|
||||
ImageDNG Type = "dng" // Adobe Digital Negative image
|
||||
ImageAVIF Type = "avif" // AV1 Image File Format (AVIF)
|
||||
ImageHEIF Type = "heif" // High Efficiency Image File Format (HEIF)
|
||||
ImageHEIC Type = "heic" // High Efficiency Image Container (HEIC)
|
||||
ImageBMP Type = "bmp" // BMP image
|
||||
ImageMPO Type = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
|
||||
ImageWebP Type = "webp" // Google WebP Image
|
||||
VideoWebM Type = "webm" // Google WebM Video
|
||||
VideoAVC Type = "avc" // H.264, Advanced Video Coding (AVC, MPEG-4 Part 10)
|
||||
VideoHEVC Type = "hevc" // H.265, High Efficiency Video Coding (HEVC)
|
||||
VideoVVC Type = "vvc" // H.266, Versatile Video Coding (VVC)
|
||||
VideoAV1 Type = "av1" // Alliance for Open Media Video
|
||||
VideoMPG Type = "mpg" // Moving Picture Experts Group (MPEG)
|
||||
VideoMJPG Type = "mjpg" // Motion JPEG (M-JPEG)
|
||||
VideoMOV Type = "mov" // QuickTime File Format, can contain AVC, HEVC,...
|
||||
VideoMP2 Type = "mp2" // MPEG-2, H.222/H.262
|
||||
VideoMP4 Type = "mp4" // MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
|
||||
VideoAVI Type = "avi" // Microsoft Audio Video Interleave (AVI)
|
||||
Video3GP Type = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
|
||||
Video3G2 Type = "3g2" // Similar to 3GP, consumes less space & bandwidth
|
||||
VideoFlash Type = "flv" // Flash Video
|
||||
VideoMKV Type = "mkv" // Matroska Multimedia Container, free and open
|
||||
VideoAVCHD Type = "mts" // AVCHD (Advanced Video Coding High Definition)
|
||||
VideoBDAV Type = "m2ts" // Blu-ray MPEG-2 Transport Stream
|
||||
VideoOGV Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
|
||||
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
|
||||
VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
|
||||
VectorSVG Type = "svg" // Scalable Vector Graphics
|
||||
VectorAI Type = "ai" // Adobe Illustrator
|
||||
VectorPS Type = "ps" // Adobe PostScript
|
||||
VectorEPS Type = "eps" // Encapsulated PostScript
|
||||
SidecarXMP Type = "xmp" // Adobe XMP sidecar file (XML)
|
||||
SidecarAAE Type = "aae" // Apple image edits sidecar file (based on XML)
|
||||
SidecarXML Type = "xml" // XML metadata / config / sidecar file
|
||||
SidecarYAML Type = "yml" // YAML metadata / config / sidecar file
|
||||
SidecarJSON Type = "json" // JSON metadata / config / sidecar file
|
||||
SidecarText Type = "txt" // Text config / sidecar file
|
||||
SidecarMarkdown Type = "md" // Markdown text sidecar file
|
||||
UnknownType Type = "" // Unknown file
|
||||
ImageRaw Type = "raw" // RAW Image
|
||||
ImageJPEG Type = "jpg" // JPEG Image
|
||||
ImageJPEGXL Type = "jxl" // JPEG XL Image
|
||||
ImagePNG Type = "png" // PNG Image
|
||||
ImageGIF Type = "gif" // GIF Image
|
||||
ImageTIFF Type = "tiff" // TIFF Image
|
||||
ImagePSD Type = "psd" // Adobe Photoshop
|
||||
ImageDNG Type = "dng" // Adobe Digital Negative image
|
||||
ImageAVIF Type = "avif" // AV1 Image File (AVIF)
|
||||
ImageAVIFS Type = "avifs" // AV1 Image Sequence (Animated AVIF)
|
||||
ImageHEIF Type = "heif" // High Efficiency Image File Format (HEIF)
|
||||
ImageHEIC Type = "heic" // High Efficiency Image Container (HEIC)
|
||||
ImageHEICS Type = "heics" // HEIC Image Sequence
|
||||
ImageBMP Type = "bmp" // BMP Image
|
||||
ImageMPO Type = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
|
||||
ImageWebP Type = "webp" // Google WebP Image
|
||||
VideoWebM Type = "webm" // Google WebM Video
|
||||
VideoAVC Type = "avc" // H.264, Advanced Video Coding (AVC, MPEG-4 Part 10)
|
||||
VideoHEVC Type = "hevc" // H.265, High Efficiency Video Coding (HEVC)
|
||||
VideoVVC Type = "vvc" // H.266, Versatile Video Coding (VVC)
|
||||
VideoAV1 Type = "av1" // Alliance for Open Media Video
|
||||
VideoMPG Type = "mpg" // Moving Picture Experts Group (MPEG)
|
||||
VideoMJPG Type = "mjpg" // Motion JPEG (M-JPEG)
|
||||
VideoMOV Type = "mov" // QuickTime File Format, can contain AVC, HEVC,...
|
||||
VideoMP2 Type = "mp2" // MPEG-2, H.222/H.262
|
||||
VideoMP4 Type = "mp4" // MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
|
||||
VideoAVI Type = "avi" // Microsoft Audio Video Interleave (AVI)
|
||||
Video3GP Type = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
|
||||
Video3G2 Type = "3g2" // Similar to 3GP, consumes less space & bandwidth
|
||||
VideoFlash Type = "flv" // Flash Video
|
||||
VideoMKV Type = "mkv" // Matroska Multimedia Container, free and open
|
||||
VideoAVCHD Type = "mts" // AVCHD (Advanced Video Coding High Definition)
|
||||
VideoBDAV Type = "m2ts" // Blu-ray MPEG-2 Transport Stream
|
||||
VideoOGV Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
|
||||
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
|
||||
VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
|
||||
VectorSVG Type = "svg" // Scalable Vector Graphics
|
||||
VectorAI Type = "ai" // Adobe Illustrator
|
||||
VectorPS Type = "ps" // Adobe PostScript
|
||||
VectorEPS Type = "eps" // Encapsulated PostScript
|
||||
SidecarXMP Type = "xmp" // Adobe XMP sidecar file (XML)
|
||||
SidecarAAE Type = "aae" // Apple image edits sidecar file (based on XML)
|
||||
SidecarXML Type = "xml" // XML metadata / config / sidecar file
|
||||
SidecarYAML Type = "yml" // YAML metadata / config / sidecar file
|
||||
SidecarJSON Type = "json" // JSON metadata / config / sidecar file
|
||||
SidecarText Type = "txt" // Text config / sidecar file
|
||||
SidecarMarkdown Type = "md" // Markdown text sidecar file
|
||||
UnknownType Type = "" // Unknown file
|
||||
)
|
||||
|
||||
// TypeAnimated maps animated file types to their mime type.
|
||||
var TypeAnimated = TypeMap{
|
||||
ImageGIF: MimeTypeGIF,
|
||||
ImagePNG: MimeTypeAPNG,
|
||||
ImageAVIF: MimeTypeAVIFS,
|
||||
ImageAVIFS: MimeTypeAVIFS,
|
||||
ImageHEIC: MimeTypeHEICS,
|
||||
ImageHEICS: MimeTypeHEICS,
|
||||
}
|
||||
|
|
|
@ -8,26 +8,28 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
MimeTypeUnknown = ""
|
||||
MimeTypeJpeg = "image/jpeg"
|
||||
MimeTypeJpegXL = "image/jxl"
|
||||
MimeTypePng = "image/png"
|
||||
MimeTypeAnimatedPng = "image/vnd.mozilla.apng"
|
||||
MimeTypeGif = "image/gif"
|
||||
MimeTypeBitmap = "image/bmp"
|
||||
MimeTypeTiff = "image/tiff"
|
||||
MimeTypeDNG = "image/dng"
|
||||
MimeTypeAVIF = "image/avif"
|
||||
MimeTypeHEIC = "image/heic"
|
||||
MimeTypeWebP = "image/webp"
|
||||
MimeTypeMP4 = "video/mp4"
|
||||
MimeTypeMOV = "video/quicktime"
|
||||
MimeTypeSVG = "image/svg+xml"
|
||||
MimeTypeAI = "application/vnd.adobe.illustrator"
|
||||
MimeTypePS = "application/ps"
|
||||
MimeTypeEPS = "image/eps"
|
||||
MimeTypeXML = "text/xml"
|
||||
MimeTypeJSON = "application/json"
|
||||
MimeTypeUnknown = ""
|
||||
MimeTypeJPEG = "image/jpeg"
|
||||
MimeTypeJPEGXL = "image/jxl"
|
||||
MimeTypePNG = "image/png"
|
||||
MimeTypeAPNG = "image/vnd.mozilla.apng"
|
||||
MimeTypeGIF = "image/gif"
|
||||
MimeTypeBMP = "image/bmp"
|
||||
MimeTypeTIFF = "image/tiff"
|
||||
MimeTypeDNG = "image/dng"
|
||||
MimeTypeAVIF = "image/avif"
|
||||
MimeTypeAVIFS = "image/avif-sequence"
|
||||
MimeTypeHEIC = "image/heic"
|
||||
MimeTypeHEICS = "image/heic-sequence"
|
||||
MimeTypeWebP = "image/webp"
|
||||
MimeTypeMP4 = "video/mp4"
|
||||
MimeTypeMOV = "video/quicktime"
|
||||
MimeTypeSVG = "image/svg+xml"
|
||||
MimeTypeAI = "application/vnd.adobe.illustrator"
|
||||
MimeTypePS = "application/ps"
|
||||
MimeTypeEPS = "image/eps"
|
||||
MimeTypeXML = "text/xml"
|
||||
MimeTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
// MimeType returns the mime type of a file, or an empty string if it could not be detected.
|
||||
|
@ -42,6 +44,12 @@ func MimeType(filename string) (mimeType string) {
|
|||
return MimeTypeDNG
|
||||
case ImageAVIF:
|
||||
return MimeTypeAVIF
|
||||
case ImageAVIFS:
|
||||
return MimeTypeAVIFS
|
||||
case ImageHEIC:
|
||||
return MimeTypeHEIC
|
||||
case ImageHEICS:
|
||||
return MimeTypeHEICS
|
||||
case VideoMP4:
|
||||
return MimeTypeMP4
|
||||
case VideoMOV:
|
||||
|
|
|
@ -15,7 +15,9 @@ var Formats = map[fs.Type]Type{
|
|||
fs.ImageBMP: Image,
|
||||
fs.ImageMPO: Image,
|
||||
fs.ImageAVIF: Image,
|
||||
fs.ImageAVIFS: Image,
|
||||
fs.ImageHEIC: Image,
|
||||
fs.ImageHEICS: Image,
|
||||
fs.VideoHEVC: Video,
|
||||
fs.ImageWebP: Image,
|
||||
fs.VideoWebM: Video,
|
||||
|
|
|
@ -33,8 +33,25 @@ func Numeric(s string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
// Float64 converts a string to a 64-bit floating point number or 0 if invalid.
|
||||
func Float64(s string) float64 {
|
||||
// IsFloat checks if the string represents a floating point number.
|
||||
func IsFloat(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
for _, r := range s {
|
||||
if r != '.' && r != ',' && (r < '0' || r > '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Float converts a string to a 64-bit floating point number or 0 if invalid.
|
||||
func Float(s string) float64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -22,6 +22,16 @@ func TestNumeric(t *testing.T) {
|
|||
assert.Equal(t, "0", result)
|
||||
})
|
||||
|
||||
t.Run("0.5", func(t *testing.T) {
|
||||
result := Numeric("0.5")
|
||||
assert.Equal(t, "0.5", result)
|
||||
})
|
||||
|
||||
t.Run("01:00", func(t *testing.T) {
|
||||
result := Numeric("01:00")
|
||||
assert.Equal(t, "0100", result)
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
result := Numeric(" 000123")
|
||||
assert.Equal(t, "000123", result)
|
||||
|
@ -48,6 +58,96 @@ func TestNumeric(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsFloat(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, IsFloat(""))
|
||||
})
|
||||
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
assert.True(t, IsFloat("0"))
|
||||
})
|
||||
|
||||
t.Run("0.5", func(t *testing.T) {
|
||||
assert.True(t, IsFloat("0.5"))
|
||||
})
|
||||
|
||||
t.Run("0,5", func(t *testing.T) {
|
||||
assert.True(t, IsFloat("0,5"))
|
||||
})
|
||||
|
||||
t.Run("123000.45245", func(t *testing.T) {
|
||||
assert.True(t, IsFloat("123000.45245 "))
|
||||
})
|
||||
|
||||
t.Run("123000.", func(t *testing.T) {
|
||||
assert.True(t, IsFloat("123000. "))
|
||||
})
|
||||
|
||||
t.Run("01:00", func(t *testing.T) {
|
||||
assert.False(t, IsFloat("01:00"))
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
assert.True(t, IsFloat(" 000123"))
|
||||
})
|
||||
|
||||
t.Run("Comma", func(t *testing.T) {
|
||||
assert.True(t, IsFloat(" 123,556\t "))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloat(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := Float("")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("NonNumeric", func(t *testing.T) {
|
||||
result := Float(" Screenshot ")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
result := Float("0")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("0.5", func(t *testing.T) {
|
||||
result := Float("0.5")
|
||||
assert.Equal(t, 0.5, result)
|
||||
})
|
||||
|
||||
t.Run("01:00", func(t *testing.T) {
|
||||
result := Float("01:00")
|
||||
assert.Equal(t, 100.0, result)
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
result := Float(" 000123")
|
||||
assert.Equal(t, 123.0, result)
|
||||
})
|
||||
|
||||
t.Run("WhitespacePadding", func(t *testing.T) {
|
||||
result := Float(" 123,556\t ")
|
||||
assert.Equal(t, 123.556, result)
|
||||
})
|
||||
|
||||
t.Run("PositiveFloat", func(t *testing.T) {
|
||||
result := Float("123,000.45245 ")
|
||||
assert.Equal(t, 123000.45245, result)
|
||||
})
|
||||
|
||||
t.Run("NegativeFloat", func(t *testing.T) {
|
||||
result := Float(" - 123,000.45245 ")
|
||||
assert.Equal(t, -123000.45245, result)
|
||||
})
|
||||
|
||||
t.Run("MultipleDots", func(t *testing.T) {
|
||||
result := Float("123.000.45245.44 m")
|
||||
assert.Equal(t, 1230004524544.0, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInt64(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := Int64("")
|
||||
|
@ -89,45 +189,3 @@ func TestInt64(t *testing.T) {
|
|||
assert.Equal(t, int64(1230004524544), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloat64(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := Float64("")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("NonNumeric", func(t *testing.T) {
|
||||
result := Float64(" Screenshot ")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
result := Float64("0")
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("LeadingZeros", func(t *testing.T) {
|
||||
result := Float64(" 000123")
|
||||
assert.Equal(t, 123.0, result)
|
||||
})
|
||||
|
||||
t.Run("WhitespacePadding", func(t *testing.T) {
|
||||
result := Float64(" 123,556\t ")
|
||||
assert.Equal(t, 123.556, result)
|
||||
})
|
||||
|
||||
t.Run("PositiveFloat", func(t *testing.T) {
|
||||
result := Float64("123,000.45245 ")
|
||||
assert.Equal(t, 123000.45245, result)
|
||||
})
|
||||
|
||||
t.Run("NegativeFloat", func(t *testing.T) {
|
||||
result := Float64(" - 123,000.45245 ")
|
||||
assert.Equal(t, -123000.45245, result)
|
||||
})
|
||||
|
||||
t.Run("MultipleDots", func(t *testing.T) {
|
||||
result := Float64("123.000.45245.44 m")
|
||||
assert.Equal(t, 1230004524544.0, result)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ func TimeStamp(t *time.Time) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
return t.UTC().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// NTimes converts an integer to a string in the format "n times" or returns an empty string if n is 0.
|
||||
|
|
Loading…
Reference in a new issue