Initial video support #17
Still need to add a player and index metadata. Work in progress. Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
510df88d7f
commit
a61f2384b3
25 changed files with 359 additions and 205 deletions
BIN
assets/resources/examples/gopher-video.mp4
Normal file
BIN
assets/resources/examples/gopher-video.mp4
Normal file
Binary file not shown.
|
@ -191,41 +191,6 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
|||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/story
|
||||
func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/photos/story", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var f form.Selection
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
if len(f.Photos) == 0 {
|
||||
log.Error("no photos selected")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no photos selected")})
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("marking photos as story: %#v", f.Photos)
|
||||
|
||||
entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
|
||||
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
|
||||
})
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("photos marked as story in %s", elapsed)})
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/labels/delete
|
||||
func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/labels/delete", func(c *gin.Context) {
|
||||
|
|
|
@ -165,42 +165,6 @@ func TestBatchPhotosPrivate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBatchPhotosStory(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetPhoto(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "PhotoStory")
|
||||
assert.Equal(t, "false", val.String())
|
||||
|
||||
BatchPhotosStory(router, conf)
|
||||
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/story", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
|
||||
val2 := gjson.Get(r2.Body.String(), "message")
|
||||
assert.Contains(t, val2.String(), "photos marked as story")
|
||||
assert.Equal(t, http.StatusOK, r2.Code)
|
||||
|
||||
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
|
||||
assert.Equal(t, http.StatusOK, r3.Code)
|
||||
val3 := gjson.Get(r3.Body.String(), "PhotoStory")
|
||||
assert.Equal(t, "true", val3.String())
|
||||
})
|
||||
t.Run("no photos selected", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
BatchPhotosStory(router, conf)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/story", `{"photos": []}`)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "No photos selected", val.String())
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
BatchPhotosStory(router, conf)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/story", `{"photos": 123}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBatchLabelsDelete(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
|
|
@ -89,8 +89,10 @@ func configAction(ctx *cli.Context) error {
|
|||
// External binaries
|
||||
fmt.Printf("%-25s %s\n", "sips-bin", conf.SipsBin())
|
||||
fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin())
|
||||
fmt.Printf("%-25s %s\n", "exiftool-bin", conf.ExifToolBin())
|
||||
fmt.Printf("%-25s %s\n", "heifconvert-bin", conf.HeifConvertBin())
|
||||
fmt.Printf("%-25s %s\n", "ffmpeg-bin", conf.FFmpegBin())
|
||||
fmt.Printf("%-25s %s\n", "exiftool-bin", conf.ExifToolBin())
|
||||
fmt.Printf("%-25s %t\n", "write-json", conf.WriteJson())
|
||||
|
||||
// Places / Geocoding API
|
||||
fmt.Printf("%-25s %s\n", "geocoding-api", conf.GeoCodingApi())
|
||||
|
|
|
@ -132,24 +132,38 @@ func (c *Config) ImportPath() string {
|
|||
return fs.Abs(c.params.ImportPath)
|
||||
}
|
||||
|
||||
// SipsBin returns the sips binary file name.
|
||||
// SipsBin returns the sips executable file name.
|
||||
func (c *Config) SipsBin() string {
|
||||
return findExecutable(c.params.SipsBin, "sips")
|
||||
}
|
||||
|
||||
// DarktableBin returns the darktable-cli binary file name.
|
||||
// DarktableBin returns the darktable-cli executable file name.
|
||||
func (c *Config) DarktableBin() string {
|
||||
return findExecutable(c.params.DarktableBin, "darktable-cli")
|
||||
}
|
||||
|
||||
// HeifConvertBin returns the heif-convert binary file name.
|
||||
// ExifToolBin returns the exiftool executable file name.
|
||||
func (c *Config) ExifToolBin() string {
|
||||
return findExecutable(c.params.ExifToolBin, "exiftool")
|
||||
}
|
||||
|
||||
// WriteJson returns true if exiftool should be used for exporting metadata to json sidecar files.
|
||||
func (c *Config) WriteJson() bool {
|
||||
if c.ReadOnly() || c.ExifToolBin() == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.params.WriteJson
|
||||
}
|
||||
|
||||
// HeifConvertBin returns the heif-convert executable file name.
|
||||
func (c *Config) HeifConvertBin() string {
|
||||
return findExecutable(c.params.HeifConvertBin, "heif-convert")
|
||||
}
|
||||
|
||||
// ExifToolBin returns the exiftool binary file name.
|
||||
func (c *Config) ExifToolBin() string {
|
||||
return findExecutable(c.params.ExifToolBin, "exiftool")
|
||||
// FFmpegBin returns the ffmpeg executable file name.
|
||||
func (c *Config) FFmpegBin() string {
|
||||
return findExecutable(c.params.FFmpegBin, "ffmpeg")
|
||||
}
|
||||
|
||||
// TempPath returns a temporary directory name for uploads and downloads.
|
||||
|
|
|
@ -145,27 +145,38 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.StringFlag{
|
||||
Name: "sips-bin",
|
||||
Usage: "sips cli binary `FILENAME`",
|
||||
Usage: "sips executable `FILENAME`",
|
||||
Value: "sips",
|
||||
EnvVar: "PHOTOPRISM_SIPS_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "darktable-bin",
|
||||
Usage: "darktable cli binary `FILENAME`",
|
||||
Usage: "darktable-cli executable `FILENAME`",
|
||||
Value: "darktable-cli",
|
||||
EnvVar: "PHOTOPRISM_DARKTABLE_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "heifconvert-bin",
|
||||
Usage: "heif-convert executable `FILENAME`",
|
||||
Value: "heif-convert",
|
||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ffmpeg-bin",
|
||||
Usage: "ffmpeg executable `FILENAME`",
|
||||
Value: "ffmpeg",
|
||||
EnvVar: "PHOTOPRISM_FFMPEG_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "exiftool-bin",
|
||||
Usage: "exiftool cli binary `FILENAME`",
|
||||
Usage: "exiftool executable `FILENAME`",
|
||||
Value: "exiftool",
|
||||
EnvVar: "PHOTOPRISM_EXIFTOOL_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "heifconvert-bin",
|
||||
Usage: "heif conversion cli binary `FILENAME`",
|
||||
Value: "heif-convert",
|
||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||
cli.BoolFlag{
|
||||
Name: "write-json",
|
||||
Usage: "run exiftool for exporting metadata to json sidecar files",
|
||||
EnvVar: "PHOTOPRISM_EXIFTOOL_JSON",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "http-port",
|
||||
|
|
|
@ -65,8 +65,10 @@ type Params struct {
|
|||
HttpServerPassword string `yaml:"http-password" flag:"http-password"`
|
||||
SipsBin string `yaml:"sips-bin" flag:"sips-bin"`
|
||||
DarktableBin string `yaml:"darktable-bin" flag:"darktable-bin"`
|
||||
ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"`
|
||||
HeifConvertBin string `yaml:"heifconvert-bin" flag:"heifconvert-bin"`
|
||||
FFmpegBin string `yaml:"ffmpeg-bin" flag:"ffmpeg-bin"`
|
||||
ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"`
|
||||
WriteJson bool `yaml:"write-json" flag:"write-json"`
|
||||
PIDFilename string `yaml:"pid-filename" flag:"pid-filename"`
|
||||
LogFilename string `yaml:"log-filename" flag:"log-filename"`
|
||||
DetachServer bool `yaml:"detach-server" flag:"detach-server"`
|
||||
|
|
|
@ -26,10 +26,11 @@ type File struct {
|
|||
FileMime string `gorm:"type:varbinary(64)"`
|
||||
FilePrimary bool
|
||||
FileSidecar bool
|
||||
FileVideo bool
|
||||
FileMissing bool
|
||||
FileDuplicate bool
|
||||
FilePortrait bool
|
||||
FileVideo bool
|
||||
FileLength time.Duration
|
||||
FileWidth int
|
||||
FileHeight int
|
||||
FileOrientation int
|
||||
|
|
|
@ -30,7 +30,7 @@ type Photo struct {
|
|||
PhotoResolution int `gorm:"type:SMALLINT" json:"PhotoResolution"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoVideo bool `json:"PhotoVideo"`
|
||||
PhotoLat float32 `gorm:"type:FLOAT;index;" json:"PhotoLat"`
|
||||
PhotoLng float32 `gorm:"type:FLOAT;index;" json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
|
|
|
@ -39,7 +39,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 48.519234,
|
||||
PhotoLng: 9.057997,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -93,7 +93,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: true,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 48.519234,
|
||||
PhotoLng: 9.057997,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -142,7 +142,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 48.519234,
|
||||
PhotoLng: 9.057997,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -191,7 +191,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 48.519234,
|
||||
PhotoLng: 9.057997,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -243,7 +243,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 48.519234,
|
||||
PhotoLng: 9.057997,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -296,7 +296,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: -21.342636,
|
||||
PhotoLng: 55.466944,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -345,7 +345,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 2,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: -21.342636,
|
||||
PhotoLng: 55.466944,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -394,7 +394,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: -21.342636,
|
||||
PhotoLng: 55.466944,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -443,7 +443,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -492,7 +492,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -541,7 +541,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -590,7 +590,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -639,7 +639,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -688,7 +688,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
@ -737,7 +737,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoResolution: 0,
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoAltitude: 0,
|
||||
|
|
|
@ -59,7 +59,7 @@ func (m *Photo) QualityScore() (score int) {
|
|||
score++
|
||||
}
|
||||
|
||||
if score < 3 && m.EditedAt != nil {
|
||||
if score < 3 && (m.PhotoVideo || m.EditedAt != nil) {
|
||||
score = 3
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ func TestSavePhotoForm(t *testing.T) {
|
|||
TitleSrc: "manual",
|
||||
PhotoFavorite: true,
|
||||
PhotoPrivate: true,
|
||||
PhotoStory: false,
|
||||
PhotoVideo: false,
|
||||
PhotoLat: 7.9999,
|
||||
PhotoLng: 8.8888,
|
||||
PhotoAltitude: 2,
|
||||
|
@ -52,7 +52,7 @@ func TestSavePhotoForm(t *testing.T) {
|
|||
assert.Equal(t, "manual", m.TitleSrc)
|
||||
assert.Equal(t, true, m.PhotoFavorite)
|
||||
assert.Equal(t, true, m.PhotoPrivate)
|
||||
assert.Equal(t, false, m.PhotoStory)
|
||||
assert.Equal(t, false, m.PhotoVideo)
|
||||
assert.Equal(t, float32(7.9999), m.PhotoLat)
|
||||
assert.NotNil(t, m.EditedAt)
|
||||
|
||||
|
@ -70,7 +70,7 @@ func TestPhoto_Save(t *testing.T) {
|
|||
TitleSrc: "manual",
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoStory: true,
|
||||
PhotoVideo: true,
|
||||
PhotoLat: 9.9999,
|
||||
PhotoLng: 8.8888,
|
||||
PhotoAltitude: 2,
|
||||
|
|
|
@ -27,7 +27,7 @@ type Photo struct {
|
|||
DescriptionSrc string `json:"DescriptionSrc"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoVideo bool `json:"PhotoVideo"`
|
||||
PhotoReview bool `json:"PhotoReview"`
|
||||
PhotoLat float32 `json:"PhotoLat"`
|
||||
PhotoLng float32 `json:"PhotoLng"`
|
||||
|
|
|
@ -10,7 +10,7 @@ func TestNewPhoto(t *testing.T) {
|
|||
t.Run("success", func(t *testing.T) {
|
||||
photo := Photo{TakenAt: time.Date(2008, 1, 1, 2, 0, 0, 0, time.UTC), TakenAtLocal: time.Date(2008, 1, 1, 2, 0, 0, 0, time.UTC),
|
||||
TakenSrc: "exif", TimeZone: "UTC", PhotoTitle: "Black beach", TitleSrc: "manual",
|
||||
PhotoFavorite: false, PhotoPrivate: false, PhotoStory: true, PhotoReview: false, PhotoLat: 9.9999, PhotoLng: 8.8888, PhotoAltitude: 2, PhotoIso: 5,
|
||||
PhotoFavorite: false, PhotoPrivate: false, PhotoVideo: true, PhotoReview: false, PhotoLat: 9.9999, PhotoLng: 8.8888, PhotoAltitude: 2, PhotoIso: 5,
|
||||
PhotoFocalLength: 10, PhotoFNumber: 3.3, PhotoExposure: "exposure", CameraID: uint(3), CameraSrc: "exif", LensID: uint(6), LocationID: "1234", LocationSrc: "geo",
|
||||
PlaceID: "765", PhotoCountry: "de"}
|
||||
|
||||
|
@ -28,7 +28,7 @@ func TestNewPhoto(t *testing.T) {
|
|||
assert.Equal(t, "manual", r.TitleSrc)
|
||||
assert.Equal(t, false, r.PhotoFavorite)
|
||||
assert.Equal(t, false, r.PhotoPrivate)
|
||||
assert.Equal(t, true, r.PhotoStory)
|
||||
assert.Equal(t, true, r.PhotoVideo)
|
||||
assert.Equal(t, false, r.PhotoReview)
|
||||
assert.Equal(t, float32(9.9999), r.PhotoLat)
|
||||
assert.Equal(t, float32(8.8888), r.PhotoLng)
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
@ -105,31 +107,83 @@ func (c *Convert) Start(path string) error {
|
|||
}
|
||||
|
||||
// ConvertCommand returns the command for converting files to JPEG, depending on the format.
|
||||
func (c *Convert) ConvertCommand(image *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||
if image.IsRaw() {
|
||||
func (c *Convert) ConvertCommand(mf *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||
if mf.IsRaw() {
|
||||
if c.conf.SipsBin() != "" {
|
||||
result = exec.Command(c.conf.SipsBin(), "-s", "format", "jpeg", "--out", jpegName, image.fileName)
|
||||
result = exec.Command(c.conf.SipsBin(), "-s", "format", "jpeg", "--out", jpegName, mf.FileName())
|
||||
} else if c.conf.DarktableBin() != "" {
|
||||
// Only one instance of darktable-cli allowed due to locking
|
||||
useMutex = true
|
||||
|
||||
if xmpName != "" {
|
||||
result = exec.Command(c.conf.DarktableBin(), image.fileName, xmpName, jpegName)
|
||||
result = exec.Command(c.conf.DarktableBin(), mf.FileName(), xmpName, jpegName)
|
||||
} else {
|
||||
result = exec.Command(c.conf.DarktableBin(), image.fileName, jpegName)
|
||||
result = exec.Command(c.conf.DarktableBin(), mf.FileName(), jpegName)
|
||||
}
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", image.Base(c.conf.Settings().Index.Group))
|
||||
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", mf.Base(c.conf.Settings().Index.Group))
|
||||
}
|
||||
} else if image.IsHEIF() {
|
||||
result = exec.Command(c.conf.HeifConvertBin(), image.fileName, jpegName)
|
||||
} else if mf.IsVideo() {
|
||||
result = exec.Command(c.conf.FFmpegBin(), "-i", mf.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
|
||||
} else if mf.IsHEIF() {
|
||||
result = exec.Command(c.conf.HeifConvertBin(), mf.FileName(), jpegName)
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("convert: image type not supported for conversion (%s)", image.FileType())
|
||||
return nil, useMutex, fmt.Errorf("convert: file type not supported for conversion (%s)", mf.FileType())
|
||||
}
|
||||
|
||||
return result, useMutex, nil
|
||||
}
|
||||
|
||||
// ToJson uses exiftool to export metadata to a json file.
|
||||
func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
|
||||
jsonName := fs.TypeJson.Find(mf.FileName(), c.conf.Settings().Index.Group)
|
||||
|
||||
result, err := NewMediaFile(jsonName)
|
||||
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
jsonName = mf.AbsBase(c.conf.Settings().Index.Group) + ".json"
|
||||
|
||||
if c.conf.ReadOnly() {
|
||||
return nil, fmt.Errorf("convert: metadata export to json disabled in read only mode (%s)", mf.RelativeName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
fileName := mf.RelativeName(c.conf.OriginalsPath())
|
||||
|
||||
log.Infof("convert: %s -> %s", fileName, fs.RelativeName(jsonName, c.conf.OriginalsPath()))
|
||||
|
||||
cmd := exec.Command(c.conf.ExifToolBin(), "-j", mf.FileName())
|
||||
|
||||
// Fetch command output.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run convert command.
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
return nil, errors.New(stderr.String())
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Write output to file.
|
||||
if err := ioutil.WriteFile(jsonName, []byte(out.String()), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if file exists.
|
||||
if !fs.FileExists(jsonName) {
|
||||
return nil, fmt.Errorf("convert: %s could not be created, check configuration", jsonName)
|
||||
}
|
||||
|
||||
return NewMediaFile(jsonName)
|
||||
}
|
||||
|
||||
// ToJpeg converts a single image file to JPEG if possible.
|
||||
func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
||||
if c.conf.ReadOnly() {
|
||||
|
|
|
@ -23,66 +23,170 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
}
|
||||
|
||||
conf := config.TestConfig()
|
||||
|
||||
conf.InitializeTestData(t)
|
||||
|
||||
convert := NewConvert(conf)
|
||||
|
||||
jpegFilename := conf.ImportPath() + "/fern_green.jpg"
|
||||
t.Run("gopher-video.mp4", func(t *testing.T) {
|
||||
fileName := conf.ExamplesPath() + "/gopher-video.mp4"
|
||||
outputName := conf.ExamplesPath() + "/gopher-video.jpg"
|
||||
|
||||
assert.Truef(t, fs.FileExists(jpegFilename), "file does not exist: %s", jpegFilename)
|
||||
_ = os.Remove(outputName)
|
||||
|
||||
t.Logf("Testing RAW to JPEG convert with %s", jpegFilename)
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
|
||||
jpegMediaFile, err := NewMediaFile(jpegFilename)
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
assert.Nil(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
imageJpeg, err := convert.ToJpeg(jpegMediaFile)
|
||||
jpegFile, err := convert.ToJpeg(mf)
|
||||
|
||||
assert.Empty(t, err, "ToJpeg() failed")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
infoJpeg, err := imageJpeg.MetaData()
|
||||
assert.Equal(t, jpegFile.FileName(), outputName)
|
||||
assert.Truef(t, fs.FileExists(jpegFile.FileName()), "output file does not exist: %s", jpegFile.FileName())
|
||||
|
||||
assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.FileName())
|
||||
metaData, err := jpegFile.MetaData()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), imageJpeg.FileName())
|
||||
}
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
} else {
|
||||
t.Logf("video metadata: %+v", metaData)
|
||||
}
|
||||
|
||||
assert.Equal(t, jpegFilename, imageJpeg.fileName)
|
||||
_ = os.Remove(outputName)
|
||||
})
|
||||
|
||||
assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel)
|
||||
t.Run("fern_green.jpg", func(t *testing.T) {
|
||||
jpegFilename := conf.ImportPath() + "/fern_green.jpg"
|
||||
|
||||
rawFilename := conf.ImportPath() + "/raw/IMG_2567.CR2"
|
||||
assert.Truef(t, fs.FileExists(jpegFilename), "file does not exist: %s", jpegFilename)
|
||||
|
||||
t.Logf("Testing RAW to JPEG convert with %s", rawFilename)
|
||||
t.Logf("Testing RAW to JPEG convert with %s", jpegFilename)
|
||||
|
||||
rawMediaFile, err := NewMediaFile(rawFilename)
|
||||
mf, err := NewMediaFile(jpegFilename)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), rawFilename)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
imageRaw, err := convert.ToJpeg(rawMediaFile)
|
||||
imageJpeg, err := convert.ToJpeg(mf)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), rawFilename)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, fs.FileExists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?")
|
||||
infoJpeg, err := imageJpeg.MetaData()
|
||||
|
||||
if imageRaw == nil {
|
||||
t.Fatal("imageRaw is nil")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), imageJpeg.FileName())
|
||||
}
|
||||
|
||||
assert.NotEqual(t, rawFilename, imageRaw.fileName)
|
||||
assert.Equal(t, jpegFilename, imageJpeg.fileName)
|
||||
|
||||
infoRaw, err := imageRaw.MetaData()
|
||||
assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel)
|
||||
|
||||
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel)
|
||||
rawFilename := conf.ImportPath() + "/raw/IMG_2567.CR2"
|
||||
|
||||
t.Logf("Testing RAW to JPEG convert with %s", rawFilename)
|
||||
|
||||
rawMediaFile, err := NewMediaFile(rawFilename)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), rawFilename)
|
||||
}
|
||||
|
||||
imageRaw, err := convert.ToJpeg(rawMediaFile)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), rawFilename)
|
||||
}
|
||||
|
||||
assert.True(t, fs.FileExists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?")
|
||||
|
||||
if imageRaw == nil {
|
||||
t.Fatal("imageRaw is nil")
|
||||
}
|
||||
|
||||
assert.NotEqual(t, rawFilename, imageRaw.fileName)
|
||||
|
||||
infoRaw, err := imageRaw.MetaData()
|
||||
|
||||
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvert_ToJson(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
convert := NewConvert(conf)
|
||||
|
||||
t.Run("gopher-video.mp4", func(t *testing.T) {
|
||||
fileName := conf.ExamplesPath() + "/gopher-video.mp4"
|
||||
outputName := conf.ExamplesPath() + "/gopher-video.json"
|
||||
|
||||
_ = os.Remove(outputName)
|
||||
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
assert.Falsef(t, fs.FileExists(outputName), "output file must not exist: %s", outputName)
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jsonFile, err := convert.ToJson(mf)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if jsonFile == nil {
|
||||
t.Fatal("jsonFile should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, jsonFile.FileName(), outputName)
|
||||
assert.Truef(t, fs.FileExists(jsonFile.FileName()), "output file does not exist: %s", jsonFile.FileName())
|
||||
assert.False(t, jsonFile.IsJpeg())
|
||||
assert.False(t, jsonFile.IsMedia())
|
||||
assert.False(t, jsonFile.IsVideo())
|
||||
assert.True(t, jsonFile.IsSidecar())
|
||||
|
||||
_ = os.Remove(outputName)
|
||||
})
|
||||
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
fileName := conf.ExamplesPath() + "/iphone_7.heic"
|
||||
outputName := conf.ExamplesPath() + "/iphone_7.json"
|
||||
|
||||
assert.True(t, fs.FileExists(fileName))
|
||||
assert.True(t, fs.FileExists(outputName))
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jsonFile, err := convert.ToJson(mf)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, jsonFile.FileName(), outputName)
|
||||
assert.Truef(t, fs.FileExists(jsonFile.FileName()), "output file does not exist: %s", jsonFile.FileName())
|
||||
assert.False(t, jsonFile.IsJpeg())
|
||||
assert.False(t, jsonFile.IsMedia())
|
||||
assert.False(t, jsonFile.IsVideo())
|
||||
assert.True(t, jsonFile.IsSidecar())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func TestConvert_Start(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
|
|
|
@ -116,7 +116,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
|||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
if err != nil || !mf.IsPhoto() {
|
||||
if err != nil || !mf.IsMedia() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
|||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
if err != nil || !mf.IsPhoto() {
|
||||
if err != nil || !mf.IsMedia() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -135,6 +135,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
photo.PhotoPath = filePath
|
||||
photo.PhotoName = fileBase
|
||||
|
||||
if m.IsVideo() {
|
||||
photo.PhotoVideo = true
|
||||
}
|
||||
|
||||
if !file.FilePrimary {
|
||||
if photoExists {
|
||||
if q := ind.db.Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); q.Error != nil {
|
||||
|
|
|
@ -34,6 +34,14 @@ func IndexWorker(jobs <-chan IndexJob) {
|
|||
}
|
||||
}
|
||||
|
||||
if ind.conf.WriteJson() && !f.HasJson() {
|
||||
if converted, err := ind.convert.ToJson(f); err != nil {
|
||||
log.Errorf("index: creating jpeg failed (%s)", err.Error())
|
||||
} else {
|
||||
related.Files = append(related.Files, converted)
|
||||
}
|
||||
}
|
||||
|
||||
res := ind.MediaFile(f, opt, "")
|
||||
done[f.FileName()] = true
|
||||
|
||||
|
|
|
@ -24,17 +24,17 @@ import (
|
|||
|
||||
// MediaFile represents a single photo, video or sidecar file.
|
||||
type MediaFile struct {
|
||||
fileName string
|
||||
fileType fs.FileType
|
||||
mimeType string
|
||||
dateCreated time.Time
|
||||
hash string
|
||||
checksum string
|
||||
width int
|
||||
height int
|
||||
once sync.Once
|
||||
metaData meta.Data
|
||||
location *entity.Location
|
||||
fileName string
|
||||
fileType fs.FileType
|
||||
mimeType string
|
||||
dateCreated time.Time
|
||||
hash string
|
||||
checksum string
|
||||
width int
|
||||
height int
|
||||
metaData meta.Data
|
||||
metaDataOnce sync.Once
|
||||
location *entity.Location
|
||||
}
|
||||
|
||||
// NewMediaFile returns a new media file.
|
||||
|
@ -52,7 +52,7 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
|
|||
}
|
||||
|
||||
// Stat returns the media file size and modification time.
|
||||
func (m MediaFile) Stat() (size int64, mod time.Time) {
|
||||
func (m *MediaFile) Stat() (size int64, mod time.Time) {
|
||||
s, err := os.Stat(m.FileName())
|
||||
|
||||
if err != nil {
|
||||
|
@ -301,6 +301,8 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
result.Main = resultFile
|
||||
} else if resultFile.IsImageOther() {
|
||||
result.Main = resultFile
|
||||
} else if resultFile.IsVideo() {
|
||||
result.Main = resultFile
|
||||
}
|
||||
|
||||
result.Files = append(result.Files, resultFile)
|
||||
|
@ -312,7 +314,7 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
}
|
||||
|
||||
// FileName returns the filename.
|
||||
func (m MediaFile) FileName() string {
|
||||
func (m *MediaFile) FileName() string {
|
||||
return m.fileName
|
||||
}
|
||||
|
||||
|
@ -322,12 +324,12 @@ func (m *MediaFile) SetFileName(fileName string) {
|
|||
}
|
||||
|
||||
// RelativeName returns the relative filename.
|
||||
func (m MediaFile) RelativeName(directory string) string {
|
||||
func (m *MediaFile) RelativeName(directory string) string {
|
||||
return fs.RelativeName(m.fileName, directory)
|
||||
}
|
||||
|
||||
// RelativePath returns the relative path without filename.
|
||||
func (m MediaFile) RelativePath(directory string) string {
|
||||
func (m *MediaFile) RelativePath(directory string) string {
|
||||
pathname := m.fileName
|
||||
|
||||
if i := strings.Index(pathname, directory); i == 0 {
|
||||
|
@ -348,7 +350,7 @@ func (m MediaFile) RelativePath(directory string) string {
|
|||
}
|
||||
|
||||
// RelativeBase returns the relative filename.
|
||||
func (m MediaFile) RelativeBase(directory string, stripSequence bool) string {
|
||||
func (m *MediaFile) RelativeBase(directory string, stripSequence bool) string {
|
||||
if relativePath := m.RelativePath(directory); relativePath != "" {
|
||||
return filepath.Join(relativePath, m.Base(stripSequence))
|
||||
}
|
||||
|
@ -357,17 +359,17 @@ func (m MediaFile) RelativeBase(directory string, stripSequence bool) string {
|
|||
}
|
||||
|
||||
// Directory returns the directory
|
||||
func (m MediaFile) Directory() string {
|
||||
func (m *MediaFile) Directory() string {
|
||||
return filepath.Dir(m.fileName)
|
||||
}
|
||||
|
||||
// Base returns the filename base without any extensions and path.
|
||||
func (m MediaFile) Base(stripSequence bool) string {
|
||||
func (m *MediaFile) Base(stripSequence bool) string {
|
||||
return fs.Base(m.FileName(), stripSequence)
|
||||
}
|
||||
|
||||
// AbsBase returns the directory and base filename without any extensions.
|
||||
func (m MediaFile) AbsBase(stripSequence bool) string {
|
||||
func (m *MediaFile) AbsBase(stripSequence bool) string {
|
||||
return fs.AbsBase(m.FileName(), stripSequence)
|
||||
}
|
||||
|
||||
|
@ -392,18 +394,18 @@ func (m *MediaFile) openFile() (*os.File, error) {
|
|||
}
|
||||
|
||||
// Exists checks if a media file exists by filename.
|
||||
func (m MediaFile) Exists() bool {
|
||||
func (m *MediaFile) Exists() bool {
|
||||
return fs.FileExists(m.FileName())
|
||||
}
|
||||
|
||||
// Remove a media file.
|
||||
func (m MediaFile) Remove() error {
|
||||
func (m *MediaFile) Remove() error {
|
||||
return os.Remove(m.FileName())
|
||||
}
|
||||
|
||||
// HasSameName compares a media file with another media file and returns if
|
||||
// their filenames are matching or not.
|
||||
func (m MediaFile) HasSameName(f *MediaFile) bool {
|
||||
func (m *MediaFile) HasSameName(f *MediaFile) bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
|
@ -465,12 +467,12 @@ func (m *MediaFile) Copy(destinationFilename string) error {
|
|||
}
|
||||
|
||||
// Extension returns the filename extension of this media file.
|
||||
func (m MediaFile) Extension() string {
|
||||
func (m *MediaFile) Extension() string {
|
||||
return strings.ToLower(filepath.Ext(m.fileName))
|
||||
}
|
||||
|
||||
// IsJpeg return true if this media file is a JPEG image.
|
||||
func (m MediaFile) IsJpeg() bool {
|
||||
func (m *MediaFile) IsJpeg() bool {
|
||||
// Don't import/use existing thumbnail files (we create our own)
|
||||
if m.Extension() == ".thm" {
|
||||
return false
|
||||
|
@ -479,18 +481,23 @@ func (m MediaFile) IsJpeg() bool {
|
|||
return m.MimeType() == fs.MimeTypeJpeg
|
||||
}
|
||||
|
||||
// IsJson return true if this media file is a json sidecar file.
|
||||
func (m *MediaFile) IsJson() bool {
|
||||
return m.HasFileType(fs.TypeJson)
|
||||
}
|
||||
|
||||
// FileType returns the file type (jpg, gif, tiff,...).
|
||||
func (m MediaFile) FileType() fs.FileType {
|
||||
func (m *MediaFile) FileType() fs.FileType {
|
||||
return fs.GetFileType(m.fileName)
|
||||
}
|
||||
|
||||
// MediaType returns the media type (video, image, raw, sidecar,...).
|
||||
func (m MediaFile) MediaType() fs.MediaType {
|
||||
func (m *MediaFile) MediaType() fs.MediaType {
|
||||
return fs.GetMediaType(m.fileName)
|
||||
}
|
||||
|
||||
// HasFileType returns true if this media file is of a given type.
|
||||
func (m MediaFile) HasFileType(t fs.FileType) bool {
|
||||
// HasFileType returns true if this is the given type.
|
||||
func (m *MediaFile) HasFileType(t fs.FileType) bool {
|
||||
if t == fs.TypeJpeg {
|
||||
return m.IsJpeg()
|
||||
}
|
||||
|
@ -498,23 +505,23 @@ func (m MediaFile) HasFileType(t fs.FileType) bool {
|
|||
return m.FileType() == t
|
||||
}
|
||||
|
||||
// IsRaw returns true if this media file a RAW file.
|
||||
func (m MediaFile) IsRaw() bool {
|
||||
// IsRaw returns true if this is a RAW file.
|
||||
func (m *MediaFile) IsRaw() bool {
|
||||
return m.HasFileType(fs.TypeRaw)
|
||||
}
|
||||
|
||||
// IsPng returns true if this media file a PNG file.
|
||||
func (m MediaFile) IsPng() bool {
|
||||
// IsPng returns true if this is a PNG file.
|
||||
func (m *MediaFile) IsPng() bool {
|
||||
return m.HasFileType(fs.TypePng)
|
||||
}
|
||||
|
||||
// IsTiff returns true if this media file a TIFF file.
|
||||
func (m MediaFile) IsTiff() bool {
|
||||
// IsTiff returns true if this is a TIFF file.
|
||||
func (m *MediaFile) IsTiff() bool {
|
||||
return m.HasFileType(fs.TypeTiff)
|
||||
}
|
||||
|
||||
// IsImageOther returns true this media file a PNG, GIF, BMP or TIFF file.
|
||||
func (m MediaFile) IsImageOther() bool {
|
||||
// IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file.
|
||||
func (m *MediaFile) IsImageOther() bool {
|
||||
switch m.FileType() {
|
||||
case fs.TypeBitmap:
|
||||
return true
|
||||
|
@ -529,31 +536,36 @@ func (m MediaFile) IsImageOther() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// IsHEIF returns true if this media file is a High Efficiency Image File Format file.
|
||||
func (m MediaFile) IsHEIF() bool {
|
||||
// IsHEIF returns true if this is a High Efficiency Image File Format file.
|
||||
func (m *MediaFile) IsHEIF() bool {
|
||||
return m.HasFileType(fs.TypeHEIF)
|
||||
}
|
||||
|
||||
// IsXMP returns true if this file is a XMP sidecar file.
|
||||
func (m MediaFile) IsXMP() bool {
|
||||
// IsXMP returns true if this is a XMP sidecar file.
|
||||
func (m *MediaFile) IsXMP() bool {
|
||||
return m.FileType() == fs.TypeXMP
|
||||
}
|
||||
|
||||
// IsSidecar returns true if this media file is a sidecar file (containing metadata).
|
||||
func (m MediaFile) IsSidecar() bool {
|
||||
// IsSidecar returns true if this is a sidecar file (containing metadata).
|
||||
func (m *MediaFile) IsSidecar() bool {
|
||||
return m.MediaType() == fs.MediaSidecar
|
||||
}
|
||||
|
||||
// IsVideo returns true if this media file is a video file.
|
||||
func (m MediaFile) IsVideo() bool {
|
||||
// IsVideo returns true if this is a video file.
|
||||
func (m *MediaFile) IsVideo() bool {
|
||||
return m.MediaType() == fs.MediaVideo
|
||||
}
|
||||
|
||||
// IsPhoto checks if this media file is a photo / image.
|
||||
func (m MediaFile) IsPhoto() bool {
|
||||
// IsPhoto returns true if this file is a photo / image.
|
||||
func (m *MediaFile) IsPhoto() bool {
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsImageOther()
|
||||
}
|
||||
|
||||
// IsMedia returns true if this is a media file (photo or video, not sidecar or other).
|
||||
func (m *MediaFile) IsMedia() bool {
|
||||
return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsHEIF() || m.IsImageOther()
|
||||
}
|
||||
|
||||
// Jpeg returns a the JPEG version of an image or sidecar file (if exists).
|
||||
func (m *MediaFile) Jpeg() (*MediaFile, error) {
|
||||
if m.IsJpeg() {
|
||||
|
@ -573,7 +585,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
|
|||
return NewMediaFile(jpegFilename)
|
||||
}
|
||||
|
||||
// HasJpeg returns false if there is no jpeg representation of this media file.
|
||||
// HasJpeg returns true if this file has or is a jpeg media file.
|
||||
func (m *MediaFile) HasJpeg() bool {
|
||||
if m.IsJpeg() {
|
||||
return true
|
||||
|
@ -582,6 +594,15 @@ func (m *MediaFile) HasJpeg() bool {
|
|||
return fs.TypeJpeg.Find(m.FileName(), false) != ""
|
||||
}
|
||||
|
||||
// HasJson returns true if this file has or is a json sidecar file.
|
||||
func (m *MediaFile) HasJson() bool {
|
||||
if m.IsJson() {
|
||||
return true
|
||||
}
|
||||
|
||||
return fs.TypeJson.Find(m.FileName(), false) != ""
|
||||
}
|
||||
|
||||
func (m *MediaFile) decodeDimensions() error {
|
||||
if !m.IsPhoto() {
|
||||
return fmt.Errorf("not a photo: %s", m.FileName())
|
||||
|
|
|
@ -6,6 +6,6 @@ import (
|
|||
|
||||
// MetaData returns exif meta data of a media file.
|
||||
func (m *MediaFile) MetaData() (result meta.Data, err error) {
|
||||
m.once.Do(func() { m.metaData, err = meta.Exif(m.FileName()) })
|
||||
m.metaDataOnce.Do(func() { m.metaData, err = meta.Exif(m.FileName()) })
|
||||
return m.metaData, err
|
||||
}
|
||||
|
|
|
@ -63,7 +63,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.BatchPhotosArchive(v1, conf)
|
||||
api.BatchPhotosRestore(v1, conf)
|
||||
api.BatchPhotosPrivate(v1, conf)
|
||||
api.BatchPhotosStory(v1, conf)
|
||||
api.BatchAlbumsDelete(v1, conf)
|
||||
api.BatchLabelsDelete(v1, conf)
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ func (s *Sync) download(a entity.Account) (complete bool, err error) {
|
|||
|
||||
mf, err := photoprism.NewMediaFile(baseDir + file.RemoteName)
|
||||
|
||||
if err != nil || !mf.IsPhoto() {
|
||||
if err != nil || !mf.IsMedia() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,11 @@ func TestGetMediaType(t *testing.T) {
|
|||
assert.Equal(t, MediaRaw, result)
|
||||
})
|
||||
|
||||
t.Run("video", func(t *testing.T) {
|
||||
result := GetMediaType("testdata/gopher.mp4")
|
||||
assert.Equal(t, MediaVideo, result)
|
||||
})
|
||||
|
||||
t.Run("sidecar", func(t *testing.T) {
|
||||
result := GetMediaType("/IMG_4120.AAE")
|
||||
assert.Equal(t, MediaSidecar, result)
|
||||
|
|
Loading…
Reference in a new issue