mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
Live Photos: Reset duration and improve type checks when indexing #5089
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -8180,12 +8180,8 @@
|
||||
1000000000,
|
||||
60000000000,
|
||||
3600000000000,
|
||||
1,
|
||||
1000,
|
||||
1000000,
|
||||
1000000000,
|
||||
60000000000,
|
||||
3600000000000,
|
||||
-9223372036854775808,
|
||||
9223372036854775807,
|
||||
1,
|
||||
1000,
|
||||
1000000,
|
||||
@@ -8202,12 +8198,8 @@
|
||||
"Second",
|
||||
"Minute",
|
||||
"Hour",
|
||||
"Nanosecond",
|
||||
"Microsecond",
|
||||
"Millisecond",
|
||||
"Second",
|
||||
"Minute",
|
||||
"Hour",
|
||||
"minDuration",
|
||||
"maxDuration",
|
||||
"Nanosecond",
|
||||
"Microsecond",
|
||||
"Millisecond",
|
||||
|
@@ -367,11 +367,16 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the video duration if this is a forced rescan.
|
||||
if o.Rescan && photoUID == "" {
|
||||
photo.PhotoDuration = 0
|
||||
}
|
||||
|
||||
// Reset file perceptive diff and chroma percent.
|
||||
file.FileDiff = -1
|
||||
file.FileChroma = -1
|
||||
file.FileVideo = m.IsVideo()
|
||||
file.MediaType = m.Media().String()
|
||||
file.MediaType = m.MediaType().String()
|
||||
|
||||
// Handle file types.
|
||||
switch {
|
||||
@@ -455,14 +460,10 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
}
|
||||
}
|
||||
|
||||
// If the photo contains an animation or has a video,
|
||||
// change the photo type from image to animated or live.
|
||||
if photo.HasMediaType(media.Image) {
|
||||
if m.IsAnimatedImage() {
|
||||
photo.SetMediaType(media.Animated, entity.SrcAuto)
|
||||
} else if m.IsLive() {
|
||||
photo.SetMediaType(media.Live, entity.SrcAuto)
|
||||
}
|
||||
// If the file contains multiple images for an animation,
|
||||
// change the media type to "animated".
|
||||
if photo.HasMediaType(media.Image) && m.IsAnimatedImage() {
|
||||
photo.SetMediaType(media.Animated, entity.SrcAuto)
|
||||
}
|
||||
case m.IsXMP():
|
||||
if data, dataErr := meta.XMP(m.FileName()); dataErr == nil {
|
||||
@@ -573,13 +574,14 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
photo.SetExposure(m.FocalLength(), m.FNumber(), m.Iso(), m.Exposure(), entity.SrcMeta)
|
||||
}
|
||||
|
||||
// Update photo type if an image and not manually modified.
|
||||
// If the media type is still set to "image" and has not been
|
||||
// manually modified, then check and update it as needed.
|
||||
if photo.HasMediaType(media.Image) {
|
||||
if m.IsAnimatedImage() {
|
||||
photo.SetMediaType(media.Animated, entity.SrcAuto)
|
||||
} else if m.IsRaw() {
|
||||
photo.SetMediaType(media.Raw, entity.SrcAuto)
|
||||
} else if m.IsLive() {
|
||||
} else if m.IsLive(photo.PhotoDuration) {
|
||||
photo.SetMediaType(media.Live, entity.SrcAuto)
|
||||
} else if m.IsVector() {
|
||||
photo.SetMediaType(media.Vector, entity.SrcAuto)
|
||||
@@ -756,8 +758,9 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
photo.SetExposure(m.FocalLength(), m.FNumber(), m.Iso(), m.Exposure(), entity.SrcMeta)
|
||||
}
|
||||
|
||||
// Set photo media type to "live" or "video".
|
||||
if m.IsLive() {
|
||||
// Set the media type to "live" instead of "video" if the video duration
|
||||
// is less than 3.1 seconds and a JPEG or HEIC image exists.
|
||||
if photo.PhotoDuration > 0 && m.IsLive(photo.PhotoDuration) {
|
||||
photo.SetMediaType(media.Live, entity.SrcAuto)
|
||||
} else {
|
||||
photo.SetMediaType(media.Video, entity.SrcAuto)
|
||||
|
@@ -938,14 +938,22 @@ func (m *MediaFile) CheckType() error {
|
||||
return fmt.Errorf("has an invalid extension for media type %s", clean.LogQuote(mimeType))
|
||||
}
|
||||
|
||||
// Media returns the media content type (video, image, raw, sidecar,...).
|
||||
func (m *MediaFile) Media() media.Type {
|
||||
// MediaType returns the media content type, e.g. video, image, raw, or sidecar.
|
||||
func (m *MediaFile) MediaType() media.Type {
|
||||
return media.FromName(m.fileName)
|
||||
}
|
||||
|
||||
// HasMediaType checks if the file has is the given media type.
|
||||
func (m *MediaFile) HasMediaType(mediaType media.Type) bool {
|
||||
return m.Media() == mediaType
|
||||
// HasMediaType checks if the file has any of the given media types.
|
||||
func (m *MediaFile) HasMediaType(mediaTypes ...media.Type) bool {
|
||||
mediaType := m.MediaType()
|
||||
|
||||
for _, t := range mediaTypes {
|
||||
if mediaType == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasFileType checks if the file has the given file type.
|
||||
@@ -992,9 +1000,14 @@ func (m *MediaFile) IsVideo() bool {
|
||||
return m.HasMediaType(media.Video)
|
||||
}
|
||||
|
||||
// IsMov returns true if this is a MOV (QuickTime) video file.
|
||||
func (m *MediaFile) IsMov() bool {
|
||||
return fs.FileType(m.fileName) == fs.VideoMov
|
||||
}
|
||||
|
||||
// IsSidecar checks if the file is a metadata sidecar file, independent of the storage location.
|
||||
func (m *MediaFile) IsSidecar() bool {
|
||||
return !m.Media().IsMain()
|
||||
return !m.MediaType().IsMain()
|
||||
}
|
||||
|
||||
// IsArchive returns true if this is an archive file.
|
||||
@@ -1063,22 +1076,38 @@ func (m *MediaFile) IsImageNative() bool {
|
||||
}
|
||||
|
||||
// IsLive checks if the file is a live photo.
|
||||
func (m *MediaFile) IsLive() bool {
|
||||
func (m *MediaFile) IsLive(videoDuration time.Duration) bool {
|
||||
if !m.InOriginals() {
|
||||
// Live Photos must be located in the Originals folder.
|
||||
return false
|
||||
} else if !m.HasMediaType(media.Video, media.Image, media.Live) {
|
||||
// Live Photos may only consist of video, image, or live files.
|
||||
return false
|
||||
} else if videoDuration > media.LiveMaxDuration {
|
||||
// Live Photos can include a maximum of 3.1 seconds of video.
|
||||
return false
|
||||
}
|
||||
|
||||
if m.HasFileType(fs.VideoMov) {
|
||||
if fs.ImageHeic.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" ||
|
||||
fs.ImageJpeg.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" {
|
||||
return true
|
||||
// Check for related image or video files in the expected formats.
|
||||
switch m.MediaType() {
|
||||
case media.Video:
|
||||
// Live Photos may only have MOV video sidecar files.
|
||||
if m.IsMov() {
|
||||
if fs.ImageHeic.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" ||
|
||||
fs.ImageJpeg.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if m.IsHeic() || m.IsJpeg() {
|
||||
if fs.VideoMov.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" {
|
||||
return true
|
||||
case media.Image:
|
||||
// Live Photos must be either HEIC or JPEG image files.
|
||||
if m.IsHeic() || m.IsJpeg() {
|
||||
if fs.VideoMov.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above applies, check the metadata for embedded videos.
|
||||
return m.MetaData().MediaType == media.Live && m.VideoInfo().Compatible
|
||||
}
|
||||
|
||||
|
@@ -150,6 +150,11 @@ func TestMediaFile_CreateExifToolJson(t *testing.T) {
|
||||
assert.True(t, mediaFile.IsM2TS())
|
||||
assert.True(t, mediaFile.IsVideo())
|
||||
assert.True(t, mediaFile.HasMediaType(media.Video))
|
||||
assert.True(t, mediaFile.HasMediaType(media.Video, media.Image))
|
||||
assert.False(t, mediaFile.HasMediaType(media.Image))
|
||||
assert.False(t, mediaFile.HasMediaType(media.Live))
|
||||
assert.False(t, mediaFile.HasMediaType())
|
||||
assert.Equal(t, media.Video, mediaFile.MediaType())
|
||||
assert.Equal(t, fs.VideoM2TS, mediaFile.FileType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.MimeType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.ContentType())
|
||||
|
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/media/http/header"
|
||||
)
|
||||
|
||||
@@ -689,73 +690,79 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
c := config.TestConfig()
|
||||
|
||||
t.Run("elephants.jpg", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "image/jpeg", mediaFile.MimeType())
|
||||
assert.Equal(t, "image/jpeg", f.MimeType())
|
||||
})
|
||||
t.Run("canon_eos_6d.dng", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/canon_eos_6d.dng")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/canon_eos_6d.dng")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "image/dng", mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.IsDng())
|
||||
assert.True(t, mediaFile.IsRaw())
|
||||
assert.Equal(t, "image/dng", f.MimeType())
|
||||
assert.True(t, f.IsDng())
|
||||
assert.True(t, f.IsRaw())
|
||||
})
|
||||
t.Run("iphone_7.xmp", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.xmp")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.xmp")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, fs.SameType(header.ContentTypeText, mediaFile.BaseType()))
|
||||
assert.Equal(t, "text/plain", mediaFile.BaseType())
|
||||
assert.Equal(t, "text/plain; charset=utf-8", mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.HasMimeType("text/plain"))
|
||||
assert.True(t, fs.SameType(header.ContentTypeText, f.BaseType()))
|
||||
assert.Equal(t, "text/plain", f.BaseType())
|
||||
assert.Equal(t, "text/plain; charset=utf-8", f.MimeType())
|
||||
assert.True(t, f.HasMimeType("text/plain"))
|
||||
})
|
||||
t.Run("iphone_7.json", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.json")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.True(t, fs.SameType(header.ContentTypeJson, mediaFile.MimeType()))
|
||||
assert.True(t, fs.SameType(header.ContentTypeJson, f.MimeType()))
|
||||
})
|
||||
t.Run("fox.profile0.8bpc.yuv420.avif", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/fox.profile0.8bpc.yuv420.avif")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/fox.profile0.8bpc.yuv420.avif")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.True(t, fs.SameType(header.ContentTypeAvif, mediaFile.MimeType()))
|
||||
assert.True(t, mediaFile.IsAvif())
|
||||
assert.True(t, fs.SameType(header.ContentTypeAvif, f.MimeType()))
|
||||
assert.True(t, f.IsAvif())
|
||||
})
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.True(t, fs.SameType(header.ContentTypeHeic, mediaFile.MimeType()))
|
||||
assert.True(t, mediaFile.IsHeic())
|
||||
assert.True(t, fs.SameType(header.ContentTypeHeic, f.MimeType()))
|
||||
assert.True(t, f.IsHeic())
|
||||
assert.False(t, f.IsMov())
|
||||
assert.False(t, f.IsVideo())
|
||||
})
|
||||
t.Run("IMG_4120.AAE", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/IMG_4120.AAE")
|
||||
f, err := NewMediaFile(c.ExamplesPath() + "/IMG_4120.AAE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, fs.SameType(header.ContentTypeXml, mediaFile.BaseType()))
|
||||
assert.Equal(t, "text/xml", mediaFile.BaseType())
|
||||
assert.Equal(t, "text/xml; charset=utf-8", mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.HasMimeType("text/xml"))
|
||||
assert.True(t, fs.SameType(header.ContentTypeXml, f.BaseType()))
|
||||
assert.Equal(t, "text/xml", f.BaseType())
|
||||
assert.Equal(t, "text/xml; charset=utf-8", f.MimeType())
|
||||
assert.True(t, f.HasMimeType("text/xml"))
|
||||
assert.False(t, f.IsMov())
|
||||
})
|
||||
t.Run("earth.mov", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "earth.mov")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "video/quicktime", f.MimeType())
|
||||
assert.False(t, f.IsHeic())
|
||||
assert.True(t, f.IsMov())
|
||||
assert.True(t, f.IsVideo())
|
||||
}
|
||||
})
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
@@ -763,6 +770,9 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "video/mp4", f.MimeType())
|
||||
assert.False(t, f.IsHeic())
|
||||
assert.False(t, f.IsMov())
|
||||
assert.True(t, f.IsVideo())
|
||||
}
|
||||
})
|
||||
t.Run("bear.m2ts", func(t *testing.T) {
|
||||
@@ -1528,7 +1538,7 @@ func TestMediaFile_IsLive(t *testing.T) {
|
||||
if f, err := NewMediaFile(fileName); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.False(t, f.IsLive()) // Image is not in originals path.
|
||||
assert.False(t, f.IsLive(media.LiveMaxDuration)) // Image is not in originals path.
|
||||
assert.False(t, f.IsRaw())
|
||||
assert.True(t, f.IsImage())
|
||||
assert.False(t, f.IsVideo())
|
||||
@@ -1540,7 +1550,7 @@ func TestMediaFile_IsLive(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "christmas.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.False(t, f.IsLive())
|
||||
assert.False(t, f.IsLive(time.Second*3))
|
||||
assert.False(t, f.IsRaw())
|
||||
assert.False(t, f.IsImage())
|
||||
assert.True(t, f.IsVideo())
|
||||
@@ -1552,7 +1562,7 @@ func TestMediaFile_IsLive(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "canon_eos_6d.dng")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.False(t, f.IsLive())
|
||||
assert.False(t, f.IsLive(time.Second*3))
|
||||
assert.True(t, f.IsRaw())
|
||||
assert.False(t, f.IsImage())
|
||||
assert.False(t, f.IsVideo())
|
||||
@@ -1564,7 +1574,7 @@ func TestMediaFile_IsLive(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "iphone_7.json")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.False(t, f.IsLive())
|
||||
assert.False(t, f.IsLive(time.Second*3))
|
||||
assert.False(t, f.IsRaw())
|
||||
assert.False(t, f.IsImage())
|
||||
assert.False(t, f.IsVideo())
|
||||
|
6
pkg/media/live.go
Normal file
6
pkg/media/live.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package media
|
||||
|
||||
import "time"
|
||||
|
||||
// LiveMaxDuration is the maximum duration for a video to play inline like a live photo.
|
||||
var LiveMaxDuration = time.Millisecond * 3100
|
Reference in New Issue
Block a user