photoprism/mediafile.go

427 lines
7.5 KiB
Go
Raw Normal View History

2018-02-04 16:34:07 +00:00
package photoprism
import (
"encoding/hex"
"github.com/brett-lempereur/ish"
2018-02-28 09:08:49 +00:00
"github.com/djherbis/times"
2018-09-06 12:47:32 +00:00
"github.com/pkg/errors"
2018-02-28 09:08:49 +00:00
"github.com/steakknife/hamming"
2018-09-06 12:47:32 +00:00
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
2018-07-18 13:17:56 +00:00
"io"
2018-02-28 09:08:49 +00:00
"log"
2018-09-06 12:47:32 +00:00
"math"
2018-02-04 16:34:07 +00:00
"net/http"
"os"
2018-02-28 09:08:49 +00:00
"path/filepath"
2018-02-04 16:34:07 +00:00
"strings"
"time"
)
const (
FileTypeOther = ""
FileTypeYaml = "yml"
FileTypeJpeg = "jpg"
FileTypeRaw = "raw"
FileTypeXmp = "xmp"
FileTypeAae = "aae"
FileTypeMovie = "mov"
)
const (
MimeTypeJpeg = "image/jpeg"
)
2018-02-28 09:08:49 +00:00
var FileExtensions = map[string]string{
".crw": FileTypeRaw,
".cr2": FileTypeRaw,
".nef": FileTypeRaw,
".arw": FileTypeRaw,
".dng": FileTypeRaw,
".mov": FileTypeMovie,
".avi": FileTypeMovie,
".yml": FileTypeYaml,
".jpg": FileTypeJpeg,
2018-07-20 08:54:31 +00:00
".thm": FileTypeJpeg,
2018-02-04 16:34:07 +00:00
".jpeg": FileTypeJpeg,
2018-02-28 09:08:49 +00:00
".xmp": FileTypeXmp,
".aae": FileTypeAae,
2018-02-04 16:34:07 +00:00
}
type MediaFile struct {
2018-02-28 09:08:49 +00:00
filename string
dateCreated time.Time
hash string
fileType string
mimeType string
perceptualHash string
2018-02-28 09:08:49 +00:00
tags []string
2018-08-07 18:17:14 +00:00
width int
height int
2018-08-09 21:10:05 +00:00
exifData *ExifData
location *Location
2018-02-04 16:34:07 +00:00
}
func NewMediaFile(filename string) *MediaFile {
instance := &MediaFile{
filename: filename,
fileType: FileTypeOther,
2018-02-28 09:08:49 +00:00
}
2018-02-04 16:34:07 +00:00
return instance
}
func (m *MediaFile) GetDateCreated() time.Time {
if !m.dateCreated.IsZero() {
return m.dateCreated
2018-02-04 16:34:07 +00:00
}
m.dateCreated = time.Now()
info, err := m.GetExifData()
2018-02-04 16:34:07 +00:00
if err == nil && !info.DateTime.IsZero() {
m.dateCreated = info.DateTime
return m.dateCreated
2018-02-04 16:34:07 +00:00
}
t, err := times.Stat(m.GetFilename())
2018-02-04 16:34:07 +00:00
if err != nil {
log.Println(err.Error())
return m.dateCreated
2018-02-04 16:34:07 +00:00
}
if t.HasBirthTime() {
m.dateCreated = t.BirthTime()
} else {
m.dateCreated = t.ModTime()
2018-02-04 16:34:07 +00:00
}
return m.dateCreated
2018-02-04 16:34:07 +00:00
}
2018-02-28 09:08:49 +00:00
func (m *MediaFile) GetCameraModel() string {
info, err := m.GetExifData()
2018-02-04 16:34:07 +00:00
var result string
if err == nil {
result = info.CameraModel
}
return result
}
func (m *MediaFile) GetCanonicalName() string {
dateCreated := m.GetDateCreated().UTC()
2018-02-04 16:34:07 +00:00
result := dateCreated.Format("20060102_150405_") + strings.ToUpper(m.GetHash()[:12])
2018-02-04 16:34:07 +00:00
return result
}
2018-07-20 08:54:31 +00:00
func (m *MediaFile) GetCanonicalNameFromFile() string {
basename := filepath.Base(m.GetFilename())
if end := strings.Index(basename, "."); end != -1 {
return basename[:end] // Length of canonical name: 16 + 12
} else {
return basename
}
}
func (m *MediaFile) GetPerceptualHash() (string, error) {
if m.perceptualHash != "" {
return m.perceptualHash, nil
}
2018-02-04 16:34:07 +00:00
hasher := ish.NewDifferenceHash(8, 8)
img, _, err := ish.LoadFile(m.GetFilename())
2018-02-04 16:34:07 +00:00
if err != nil {
return "", err
}
dh, err := hasher.Hash(img)
if err != nil {
return "", err
}
m.perceptualHash = hex.EncodeToString(dh)
2018-02-04 16:34:07 +00:00
return m.perceptualHash, nil
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) GetPerceptualDistance(perceptualHash string) (int, error) {
var hash1, hash2 []byte
if imageHash, err := m.GetPerceptualHash(); err != nil {
return -1, err
} else {
if decoded, err := hex.DecodeString(imageHash); err != nil {
return -1, err
} else {
hash1 = decoded
}
2018-02-04 16:34:07 +00:00
}
if decoded, err := hex.DecodeString(perceptualHash); err != nil {
return -1, err
} else {
hash2 = decoded
}
result := hamming.Bytes(hash1, hash2)
return result, nil
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) GetHash() string {
if len(m.hash) == 0 {
m.hash = fileHash(m.GetFilename())
}
return m.hash
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) GetRelatedFiles() (result []*MediaFile, masterFile *MediaFile, err error) {
extension := m.GetExtension()
2018-02-04 16:34:07 +00:00
2018-02-28 09:08:49 +00:00
baseFilename := m.filename[0 : len(m.filename)-len(extension)]
2018-02-04 16:34:07 +00:00
matches, err := filepath.Glob(baseFilename + "*")
if err != nil {
return result, nil, err
2018-02-04 16:34:07 +00:00
}
for _, filename := range matches {
resultFile := NewMediaFile(filename)
if masterFile == nil && resultFile.IsJpeg() {
masterFile = resultFile
} else if resultFile.IsRaw() {
masterFile = resultFile
}
result = append(result, resultFile)
2018-02-04 16:34:07 +00:00
}
return result, masterFile, nil
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) GetFilename() string {
return m.filename
2018-02-04 16:34:07 +00:00
}
2018-02-28 09:08:49 +00:00
func (m *MediaFile) SetFilename(filename string) {
m.filename = filename
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) GetMimeType() string {
if m.mimeType != "" {
return m.mimeType
2018-02-04 16:34:07 +00:00
}
handle, err := m.openFile()
2018-02-04 16:34:07 +00:00
if err != nil {
log.Println("Error: Could not open file to determine mime type")
return ""
}
defer handle.Close()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
_, err = handle.Read(buffer)
if err != nil {
log.Println("Error: Could not read file to determine mime type: " + m.GetFilename())
2018-02-04 16:34:07 +00:00
return ""
}
m.mimeType = http.DetectContentType(buffer)
2018-02-04 16:34:07 +00:00
return m.mimeType
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) openFile() (*os.File, error) {
if handle, err := os.Open(m.filename); err == nil {
2018-02-04 16:34:07 +00:00
return handle, nil
} else {
log.Println(err.Error())
return nil, err
}
}
func (m *MediaFile) Exists() bool {
return fileExists(m.GetFilename())
}
func (m *MediaFile) Remove() error {
return os.Remove(m.GetFilename())
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) Move(newFilename string) error {
if err := os.Rename(m.filename, newFilename); err != nil {
2018-02-04 16:34:07 +00:00
return err
}
m.filename = newFilename
2018-02-04 16:34:07 +00:00
return nil
}
2018-06-17 18:12:02 +00:00
func (m *MediaFile) Copy(destinationFilename string) error {
file, err := m.openFile()
if err != nil {
log.Println(err.Error())
return err
}
defer file.Close()
destination, err := os.OpenFile(destinationFilename, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Println(err.Error())
return err
}
defer destination.Close()
_, err = io.Copy(destination, file)
if err != nil {
log.Println(err.Error())
return err
}
return nil
}
func (m *MediaFile) GetExtension() string {
return strings.ToLower(filepath.Ext(m.filename))
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) IsJpeg() bool {
2018-06-17 10:56:02 +00:00
// Don't import/use existing thumbnail files (we create our own)
if m.GetExtension() == ".thm" {
return false
}
return m.GetMimeType() == MimeTypeJpeg
2018-02-04 16:34:07 +00:00
}
2018-07-20 08:54:31 +00:00
func (m *MediaFile) GetType() string {
return FileExtensions[m.GetExtension()]
}
func (m *MediaFile) HasType(typeString string) bool {
2018-02-04 16:34:07 +00:00
if typeString == FileTypeJpeg {
return m.IsJpeg()
2018-02-04 16:34:07 +00:00
}
2018-07-20 08:54:31 +00:00
return m.GetType() == typeString
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) IsRaw() bool {
return m.HasType(FileTypeRaw)
2018-02-04 16:34:07 +00:00
}
func (m *MediaFile) IsPhoto() bool {
return m.IsJpeg() || m.IsRaw()
2018-02-28 09:08:49 +00:00
}
2018-07-20 08:54:31 +00:00
func (m *MediaFile) GetJpeg() (*MediaFile, error) {
if m.IsJpeg() {
return m, nil
}
2018-08-07 18:17:14 +00:00
jpegFilename := m.GetFilename()[0:len(m.GetFilename())-len(filepath.Ext(m.GetFilename()))] + ".jpg"
2018-07-20 08:54:31 +00:00
if !fileExists(jpegFilename) {
return nil, errors.New("file does not exist")
}
result := NewMediaFile(jpegFilename)
return result, nil
}
2018-08-07 18:17:14 +00:00
func (m *MediaFile) decodeDimensions() error {
if m.IsJpeg() {
file, err := os.Open(m.GetFilename())
defer file.Close()
if err != nil {
return err
}
size, _, err := image.DecodeConfig(file)
if err != nil {
return err
}
m.width = size.Width
m.height = size.Height
} else {
if exif, err := m.GetExifData(); err == nil {
m.width = exif.Width
m.height = exif.Height
} else {
return err
}
}
return nil
}
func (m *MediaFile) GetWidth() int {
if m.width <= 0 {
m.decodeDimensions()
}
return m.width
}
func (m *MediaFile) GetHeight() int {
if m.height <= 0 {
m.decodeDimensions()
}
return m.height
}
func (m *MediaFile) GetAspectRatio() float64 {
width := float64(m.GetWidth())
height := float64(m.GetHeight())
if width <= 0 || height <= 0 {
return 0
}
aspectRatio := width / height
2018-08-09 21:10:05 +00:00
return math.Round(aspectRatio*100) / 100
}
func (m *MediaFile) GetOrientation() int {
if exif, err := m.GetExifData(); err == nil {
return exif.Orientation
} else {
return 1
}
}