2019-12-11 16:55:18 +01:00
package entity
2018-07-18 15:17:56 +02:00
import (
2019-05-14 18:16:35 +02:00
"fmt"
2022-07-06 23:01:54 +02:00
"image"
2022-04-13 22:17:59 +02:00
"math"
2020-12-16 11:59:16 +01:00
"path/filepath"
2021-09-06 05:13:53 +02:00
"sort"
2022-01-06 14:33:49 +01:00
"sync"
2019-12-27 05:18:52 +01:00
"time"
2019-05-14 18:16:35 +02:00
2021-10-01 00:05:49 +02:00
"github.com/dustin/go-humanize/english"
2019-05-14 18:16:35 +02:00
"github.com/gosimple/slug"
2018-07-18 15:17:56 +02:00
"github.com/jinzhu/gorm"
2021-09-24 22:46:03 +02:00
"github.com/ulule/deepcopier"
2022-09-28 09:01:17 +02:00
"github.com/photoprism/photoprism/internal/customize"
2021-09-24 22:46:03 +02:00
"github.com/photoprism/photoprism/internal/face"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/clean"
2021-12-09 07:00:39 +01:00
"github.com/photoprism/photoprism/pkg/colors"
2020-05-25 19:10:44 +02:00
"github.com/photoprism/photoprism/pkg/fs"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/projection"
2020-01-12 14:00:56 +01:00
"github.com/photoprism/photoprism/pkg/rnd"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/txt"
2018-07-18 15:17:56 +02:00
)
2022-09-30 00:42:19 +02:00
const (
FileUID = byte ( 'f' )
)
2022-03-30 20:36:25 +02:00
// Files represents a file result set.
2020-06-01 09:45:24 +02:00
type Files [ ] File
2022-03-30 20:36:25 +02:00
// Index updates should not run simultaneously.
var fileIndexMutex = sync . Mutex { }
var filePrimaryMutex = sync . Mutex { }
2022-01-06 14:33:49 +01:00
2020-05-27 13:40:21 +02:00
// File represents an image or sidecar file that belongs to a photo.
2018-07-18 15:17:56 +02:00
type File struct {
2023-03-20 16:18:27 +01:00
ID uint ` gorm:"primary_key" json:"-" yaml:"-" `
Photo * Photo ` json:"-" yaml:"-" `
PhotoID uint ` gorm:"index:idx_files_photo_id;" json:"-" yaml:"-" `
PhotoUID string ` gorm:"type:VARBINARY(42);index;" json:"PhotoUID" yaml:"PhotoUID" `
PhotoTakenAt time . Time ` gorm:"type:DATETIME;index;" json:"TakenAt" yaml:"TakenAt" `
TimeIndex * string ` gorm:"type:VARBINARY(64);" json:"TimeIndex" yaml:"TimeIndex" `
MediaID * string ` gorm:"type:VARBINARY(32);" json:"MediaID" yaml:"MediaID" `
MediaUTC int64 ` gorm:"column:media_utc;index;" json:"MediaUTC" yaml:"MediaUTC,omitempty" `
InstanceID string ` gorm:"type:VARBINARY(64);index;" json:"InstanceID,omitempty" yaml:"InstanceID,omitempty" `
FileUID string ` gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID" `
FileName string ` gorm:"type:VARBINARY(1024);unique_index:idx_files_name_root;" json:"Name" yaml:"Name" `
FileRoot string ` gorm:"type:VARBINARY(16);default:'/';unique_index:idx_files_name_root;" json:"Root" yaml:"Root,omitempty" `
OriginalName string ` gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty" `
FileHash string ` gorm:"type:VARBINARY(128);index" json:"Hash" yaml:"Hash,omitempty" `
FileSize int64 ` json:"Size" yaml:"Size,omitempty" `
FileCodec string ` gorm:"type:VARBINARY(32)" json:"Codec" yaml:"Codec,omitempty" `
FileType string ` gorm:"type:VARBINARY(16)" json:"FileType" yaml:"FileType,omitempty" `
MediaType string ` gorm:"type:VARBINARY(16)" json:"MediaType" yaml:"MediaType,omitempty" `
FileMime string ` gorm:"type:VARBINARY(64)" json:"Mime" yaml:"Mime,omitempty" `
FilePrimary bool ` gorm:"index:idx_files_photo_id;" json:"Primary" yaml:"Primary,omitempty" `
FileSidecar bool ` json:"Sidecar" yaml:"Sidecar,omitempty" `
FileMissing bool ` json:"Missing" yaml:"Missing,omitempty" `
FilePortrait bool ` json:"Portrait" yaml:"Portrait,omitempty" `
FileVideo bool ` json:"Video" yaml:"Video,omitempty" `
FileDuration time . Duration ` json:"Duration" yaml:"Duration,omitempty" `
FileFPS float64 ` gorm:"column:file_fps;" json:"FPS" yaml:"FPS,omitempty" `
2023-08-15 14:51:32 +02:00
FileFrames int ` gorm:"column:file_frames;" json:"Frames" yaml:"Frames,omitempty" `
FileWidth int ` gorm:"column:file_width;" json:"Width" yaml:"Width,omitempty" `
FileHeight int ` gorm:"column:file_height;" json:"Height" yaml:"Height,omitempty" `
FileOrientation int ` gorm:"column:file_orientation;" json:"Orientation" yaml:"Orientation,omitempty" `
FileOrientationSrc string ` gorm:"column:file_orientation_src;type:VARBINARY(8);default:'';" json:"OrientationSrc" yaml:"OrientationSrc,omitempty" `
FileProjection string ` gorm:"column:file_projection;type:VARBINARY(64);" json:"Projection,omitempty" yaml:"Projection,omitempty" `
FileAspectRatio float32 ` gorm:"column:file_aspect_ratio;type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,omitempty" `
2023-03-20 16:18:27 +01:00
FileHDR bool ` gorm:"column:file_hdr;" json:"HDR" yaml:"HDR,omitempty" `
FileWatermark bool ` gorm:"column:file_watermark;" json:"Watermark" yaml:"Watermark,omitempty" `
FileColorProfile string ` gorm:"type:VARBINARY(64);" json:"ColorProfile,omitempty" yaml:"ColorProfile,omitempty" `
FileMainColor string ` gorm:"type:VARBINARY(16);index;" json:"MainColor" yaml:"MainColor,omitempty" `
FileColors string ` gorm:"type:VARBINARY(18);" json:"Colors" yaml:"Colors,omitempty" `
FileLuminance string ` gorm:"type:VARBINARY(18);" json:"Luminance" yaml:"Luminance,omitempty" `
FileDiff int ` json:"Diff" yaml:"Diff,omitempty" `
FileChroma int16 ` json:"Chroma" yaml:"Chroma,omitempty" `
FileSoftware string ` gorm:"type:VARCHAR(64)" json:"Software" yaml:"Software,omitempty" `
FileError string ` gorm:"type:VARBINARY(512)" json:"Error" yaml:"Error,omitempty" `
ModTime int64 ` json:"ModTime" yaml:"-" `
CreatedAt time . Time ` json:"CreatedAt" yaml:"-" `
CreatedIn int64 ` json:"CreatedIn" yaml:"-" `
UpdatedAt time . Time ` json:"UpdatedAt" yaml:"-" `
UpdatedIn int64 ` json:"UpdatedIn" yaml:"-" `
PublishedAt * time . Time ` sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty" `
DeletedAt * time . Time ` sql:"index" json:"DeletedAt,omitempty" yaml:"-" `
Share [ ] FileShare ` json:"-" yaml:"-" `
Sync [ ] FileSync ` json:"-" yaml:"-" `
markers * Markers
2019-05-14 18:16:35 +02:00
}
2022-09-28 09:01:17 +02:00
// TableName returns the entity table name.
2021-09-21 08:56:35 +02:00
func ( File ) TableName ( ) string {
return "files"
}
2022-03-30 20:36:25 +02:00
// RegenerateIndex updates the search index columns.
func ( m File ) RegenerateIndex ( ) {
fileIndexMutex . Lock ( )
defer fileIndexMutex . Unlock ( )
start := time . Now ( )
2022-07-22 19:47:16 +02:00
2022-03-30 20:36:25 +02:00
photosTable := Photo { } . TableName ( )
var updateWhere * gorm . SqlExpr
2022-04-02 19:26:28 +02:00
var scope string
2022-03-30 20:36:25 +02:00
if m . PhotoID > 0 {
2022-07-22 19:47:16 +02:00
updateWhere = gorm . Expr ( "files.photo_id = ?" , m . PhotoID )
2022-07-23 11:20:56 +02:00
scope = "index by photo id"
2022-03-30 20:36:25 +02:00
} else if m . PhotoUID != "" {
2022-07-22 19:47:16 +02:00
updateWhere = gorm . Expr ( "files.photo_uid = ?" , m . PhotoUID )
2022-07-23 11:20:56 +02:00
scope = "index by photo uid"
2022-03-30 20:36:25 +02:00
} else if m . ID > 0 {
2022-07-22 19:47:16 +02:00
updateWhere = gorm . Expr ( "files.id = ?" , m . ID )
2022-07-23 11:20:56 +02:00
scope = "index by file id"
2022-03-30 20:36:25 +02:00
} else {
2022-07-22 19:47:16 +02:00
updateWhere = gorm . Expr ( "files.photo_id IS NOT NULL" )
2022-07-23 11:20:56 +02:00
scope = "index"
2022-03-30 20:36:25 +02:00
}
switch DbDialect ( ) {
case MySQL :
Log ( "files" , "regenerate photo_taken_at" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files JOIN ? p ON p.id = files.photo_id SET files.photo_taken_at = p.taken_at_local WHERE ?" ,
gorm . Expr ( photosTable ) , updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
Log ( "files" , "regenerate media_id" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files SET media_id = CASE WHEN file_missing = 0 AND deleted_at IS NULL THEN CONCAT((10000000000 - photo_id), '-', 1 + file_sidecar - file_primary, '-', file_uid) ELSE NULL END WHERE ?" ,
updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
Log ( "files" , "regenerate time_index" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files SET time_index = CASE WHEN media_id IS NOT NULL AND photo_taken_at IS NOT NULL THEN CONCAT(100000000000000 - CAST(photo_taken_at AS UNSIGNED), '-', media_id) ELSE NULL END WHERE ?" ,
updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
case SQLite3 :
Log ( "files" , "regenerate photo_taken_at" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files SET photo_taken_at = (SELECT p.taken_at_local FROM ? p WHERE p.id = photo_id) WHERE ?" ,
gorm . Expr ( photosTable ) , updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
Log ( "files" , "regenerate media_id" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files SET media_id = CASE WHEN file_missing = 0 AND deleted_at IS NULL THEN ((10000000000 - photo_id) || '-' || (1 + file_sidecar - file_primary) || '-' || file_uid) ELSE NULL END WHERE ?" ,
updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
Log ( "files" , "regenerate time_index" ,
2022-07-22 19:47:16 +02:00
Db ( ) . Exec ( "UPDATE files SET time_index = CASE WHEN media_id IS NOT NULL AND photo_taken_at IS NOT NULL THEN ((100000000000000 - strftime('%Y%m%d%H%M%S', photo_taken_at)) || '-' || media_id) ELSE NULL END WHERE ?" ,
updateWhere ) . Error )
2022-03-30 20:36:25 +02:00
default :
log . Warnf ( "sql: unsupported dialect %s" , DbDialect ( ) )
}
2022-04-04 14:21:43 +02:00
log . Debugf ( "search: updated %s [%s]" , scope , time . Since ( start ) )
2022-03-30 20:36:25 +02:00
}
2020-02-21 01:14:45 +01:00
// FirstFileByHash gets a file in db from its hash
2020-04-30 20:07:03 +02:00
func FirstFileByHash ( fileHash string ) ( File , error ) {
2019-07-03 09:27:30 +02:00
var file File
2021-02-05 21:12:40 +01:00
res := Db ( ) . Unscoped ( ) . First ( & file , "file_hash = ?" , fileHash )
2019-07-03 09:27:30 +02:00
2021-02-05 21:12:40 +01:00
return file , res . Error
2019-07-03 09:27:30 +02:00
}
2020-07-14 11:00:49 +02:00
// PrimaryFile returns the primary file for a photo uid.
2022-10-02 11:38:30 +02:00
func PrimaryFile ( photoUid string ) ( * File , error ) {
2021-09-02 14:23:40 +02:00
file := File { }
2020-07-14 11:00:49 +02:00
2022-10-02 11:38:30 +02:00
res := Db ( ) . Unscoped ( ) . First ( & file , "file_primary = 1 AND photo_uid = ?" , photoUid )
2020-07-14 11:00:49 +02:00
2021-09-02 14:23:40 +02:00
return & file , res . Error
2020-07-14 11:00:49 +02:00
}
2020-05-23 20:58:58 +02:00
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
2019-06-04 18:26:35 +02:00
func ( m * File ) BeforeCreate ( scope * gorm . Scope ) error {
2022-04-15 09:42:07 +02:00
// Set MediaType based on FileName if empty.
if m . MediaType == "" && m . FileName != "" {
m . MediaType = media . FromName ( m . FileName ) . String ( )
}
// Set MediaUTC based on PhotoTakenAt if empty.
if m . MediaUTC == 0 && ! m . PhotoTakenAt . IsZero ( ) {
m . MediaUTC = m . PhotoTakenAt . UnixMilli ( )
}
// Return if uid exists.
2022-09-30 00:42:19 +02:00
if rnd . IsUnique ( m . FileUID , FileUID ) {
2020-05-01 12:57:26 +02:00
return nil
}
2022-09-30 00:42:19 +02:00
return scope . SetColumn ( "FileUID" , rnd . GenerateUID ( FileUID ) )
2019-06-04 18:26:35 +02:00
}
2021-01-27 21:30:10 +01:00
// DownloadName returns the download file name.
2022-09-28 09:01:17 +02:00
func ( m * File ) DownloadName ( n customize . DownloadName , seq int ) string {
2021-01-27 21:30:10 +01:00
switch n {
2022-09-28 09:01:17 +02:00
case customize . DownloadNameFile :
2021-01-27 21:30:10 +01:00
return m . Base ( seq )
2022-09-28 09:01:17 +02:00
case customize . DownloadNameOriginal :
2021-01-27 21:30:10 +01:00
return m . OriginalBase ( seq )
default :
return m . ShareBase ( seq )
}
}
2020-12-16 11:59:16 +01:00
// Base returns the file name without path.
2021-01-27 21:30:10 +01:00
func ( m * File ) Base ( seq int ) string {
2020-12-16 11:59:16 +01:00
if m . FileName == "" {
2021-01-27 21:30:10 +01:00
return m . ShareBase ( seq )
}
base := filepath . Base ( m . FileName )
if seq > 0 {
return fmt . Sprintf ( "%s (%d)%s" , fs . StripExt ( base ) , seq , filepath . Ext ( base ) )
2020-12-16 11:59:16 +01:00
}
2021-01-27 21:30:10 +01:00
return base
2020-12-16 11:59:16 +01:00
}
// OriginalBase returns the original file name without path.
2021-01-27 21:30:10 +01:00
func ( m * File ) OriginalBase ( seq int ) string {
2020-12-16 11:59:16 +01:00
if m . OriginalName == "" {
2021-01-27 21:30:10 +01:00
return m . Base ( seq )
2020-12-16 11:59:16 +01:00
}
2021-01-27 21:30:10 +01:00
base := filepath . Base ( m . OriginalName )
if seq > 0 {
return fmt . Sprintf ( "%s (%d)%s" , fs . StripExt ( base ) , seq , filepath . Ext ( base ) )
}
return base
2020-12-16 11:59:16 +01:00
}
2021-01-27 21:30:10 +01:00
// ShareBase returns a meaningful file name for sharing.
func ( m * File ) ShareBase ( seq int ) string {
2020-05-25 19:10:44 +02:00
photo := m . RelatedPhoto ( )
2019-05-14 18:16:35 +02:00
2020-05-25 19:10:44 +02:00
if photo == nil {
return fmt . Sprintf ( "%s.%s" , m . FileHash , m . FileType )
} else if len ( m . FileHash ) < 8 {
return fmt . Sprintf ( "%s.%s" , rnd . UUID ( ) , m . FileType )
} else if photo . TakenAtLocal . IsZero ( ) || photo . PhotoTitle == "" {
return fmt . Sprintf ( "%s.%s" , m . FileHash , m . FileType )
2019-05-16 04:03:55 +02:00
}
2019-05-14 18:16:35 +02:00
2022-04-15 09:42:07 +02:00
name := txt . Title ( slug . MakeLang ( photo . PhotoTitle , "en" ) )
2020-05-25 19:10:44 +02:00
taken := photo . TakenAtLocal . Format ( "20060102-150405" )
2019-05-16 04:03:55 +02:00
2021-01-27 21:30:10 +01:00
if seq > 0 {
return fmt . Sprintf ( "%s-%s (%d).%s" , taken , name , seq , m . FileType )
}
2019-05-14 18:16:35 +02:00
2021-01-27 21:30:10 +01:00
return fmt . Sprintf ( "%s-%s.%s" , taken , name , m . FileType )
2018-07-18 15:17:56 +02:00
}
2020-02-01 20:52:28 +01:00
2020-03-25 12:39:07 +01:00
// Changed returns true if new and old file size or modified time are different.
2020-07-17 16:09:55 +02:00
func ( m File ) Changed ( fileSize int64 , modTime time . Time ) bool {
2020-11-20 17:25:46 +01:00
// File size has changed.
2020-02-01 20:52:28 +01:00
if m . FileSize != fileSize {
return true
}
2020-11-20 17:25:46 +01:00
// Modification time has changed.
2022-05-17 01:04:26 +02:00
if m . ModTime == modTime . UTC ( ) . Truncate ( time . Second ) . Unix ( ) {
2020-04-14 15:08:39 +02:00
return false
2020-02-01 20:52:28 +01:00
}
2020-04-14 15:08:39 +02:00
return true
2020-02-01 20:52:28 +01:00
}
2020-05-07 19:42:04 +02:00
2020-11-21 15:43:13 +01:00
// Missing returns true if this file is current missing or marked as deleted.
func ( m File ) Missing ( ) bool {
return m . FileMissing || m . DeletedAt != nil
}
2021-10-05 22:33:29 +02:00
// DeletePermanently permanently removes a file from the index.
2020-10-04 14:21:40 +02:00
func ( m * File ) DeletePermanently ( ) error {
2021-09-30 15:50:10 +02:00
if m . ID < 1 || m . FileUID == "" {
2022-04-15 09:42:07 +02:00
return fmt . Errorf ( "invalid file id %d / uid %s" , m . ID , clean . Log ( m . FileUID ) )
2021-09-30 15:50:10 +02:00
}
2021-09-24 13:13:59 +02:00
if err := UnscopedDb ( ) . Delete ( Marker { } , "file_uid = ?" , m . FileUID ) . Error ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while removing markers" , clean . Log ( m . FileUID ) , err )
2021-09-24 13:13:59 +02:00
}
if err := UnscopedDb ( ) . Delete ( FileShare { } , "file_id = ?" , m . ID ) . Error ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while removing share info" , clean . Log ( m . FileUID ) , err )
2021-09-24 13:13:59 +02:00
}
if err := UnscopedDb ( ) . Delete ( FileSync { } , "file_id = ?" , m . ID ) . Error ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while removing remote sync info" , clean . Log ( m . FileUID ) , err )
2021-09-24 13:13:59 +02:00
}
if err := m . ReplaceHash ( "" ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while removing covers" , clean . Log ( m . FileUID ) , err )
2021-09-24 13:13:59 +02:00
}
2020-10-04 14:21:40 +02:00
2021-09-24 13:13:59 +02:00
return UnscopedDb ( ) . Delete ( m ) . Error
}
// ReplaceHash updates file hash references.
2021-09-24 22:46:03 +02:00
func ( m * File ) ReplaceHash ( newHash string ) error {
if m . FileHash == newHash {
2021-09-24 13:13:59 +02:00
// Nothing to do.
return nil
}
2021-09-24 22:46:03 +02:00
// Log values.
if m . FileHash != "" && newHash == "" {
2022-04-15 09:42:07 +02:00
log . Tracef ( "file %s: removing hash %s" , clean . Log ( m . FileUID ) , clean . Log ( m . FileHash ) )
2021-09-24 22:46:03 +02:00
} else if m . FileHash != "" && newHash != "" {
2022-04-15 09:42:07 +02:00
log . Tracef ( "file %s: hash %s changed to %s" , clean . Log ( m . FileUID ) , clean . Log ( m . FileHash ) , clean . Log ( newHash ) )
2021-10-05 20:51:18 +02:00
// Reset error when hash changes.
m . FileError = ""
2021-09-24 13:13:59 +02:00
}
2021-09-24 22:46:03 +02:00
// Set file hash to new value.
oldHash := m . FileHash
m . FileHash = newHash
// Ok to skip updating related tables?
2021-09-24 13:13:59 +02:00
if m . NoJPEG ( ) || m . FileHash == "" {
return nil
}
2021-11-21 14:05:07 +01:00
entities := Tables {
2021-09-24 13:34:37 +02:00
"albums" : Album { } ,
"labels" : Label { } ,
2021-09-24 13:13:59 +02:00
}
// Search related tables for references and update them.
for name , entity := range entities {
start := time . Now ( )
2022-04-04 08:54:03 +02:00
if res := UnscopedDb ( ) . Model ( entity ) . Where ( "thumb = ?" , oldHash ) . UpdateColumn ( "thumb" , newHash ) ; res . Error != nil {
2021-09-24 13:13:59 +02:00
return res . Error
2021-10-01 00:05:49 +02:00
} else if res . RowsAffected > 0 {
2021-10-02 14:24:44 +02:00
log . Infof ( "%s: updated %s [%s]" , name , english . Plural ( int ( res . RowsAffected ) , "cover" , "covers" ) , time . Since ( start ) )
2021-09-24 13:13:59 +02:00
}
}
return nil
2020-10-04 14:21:40 +02:00
}
// Delete deletes the entity from the database.
func ( m * File ) Delete ( permanently bool ) error {
2021-09-30 15:50:10 +02:00
if m . ID < 1 || m . FileUID == "" {
2022-04-15 09:42:07 +02:00
return fmt . Errorf ( "invalid file id %d / uid %s" , m . ID , clean . Log ( m . FileUID ) )
2021-09-30 15:50:10 +02:00
}
2020-10-04 14:21:40 +02:00
if permanently {
return m . DeletePermanently ( )
}
return Db ( ) . Delete ( m ) . Error
}
2020-05-07 19:42:04 +02:00
// Purge removes a file from the index by marking it as missing.
func ( m * File ) Purge ( ) error {
2021-08-29 13:26:05 +02:00
deletedAt := TimeStamp ( )
2020-12-11 17:21:13 +01:00
m . FileMissing = true
m . FilePrimary = false
2021-01-24 20:40:40 +01:00
m . DeletedAt = & deletedAt
return UnscopedDb ( ) . Exec ( "UPDATE files SET file_missing = 1, file_primary = 0, deleted_at = ? WHERE id = ?" , & deletedAt , m . ID ) . Error
}
// Found restores a previously purged file.
func ( m * File ) Found ( ) error {
m . FileMissing = false
m . DeletedAt = nil
return UnscopedDb ( ) . Exec ( "UPDATE files SET file_missing = 0, deleted_at = NULL WHERE id = ?" , m . ID ) . Error
2020-05-07 19:42:04 +02:00
}
2020-05-13 15:36:42 +02:00
// AllFilesMissing returns true, if all files for the photo of this file are missing.
func ( m * File ) AllFilesMissing ( ) bool {
count := 0
if err := Db ( ) . Model ( & File { } ) .
2020-05-18 16:36:24 +02:00
Where ( "photo_id = ? AND file_missing = 0" , m . PhotoID ) .
2020-05-13 15:36:42 +02:00
Count ( & count ) . Error ; err != nil {
2020-05-18 16:36:24 +02:00
log . Errorf ( "file: %s" , err . Error ( ) )
2020-05-13 15:36:42 +02:00
}
return count == 0
}
2020-07-06 19:15:57 +02:00
// Create inserts a new row to the database.
func ( m * File ) Create ( ) error {
if m . PhotoID == 0 {
2022-01-05 11:40:44 +01:00
return fmt . Errorf ( "file: cannot create file with empty photo id" )
2020-07-06 19:15:57 +02:00
}
2020-07-07 10:51:55 +02:00
if err := UnscopedDb ( ) . Create ( m ) . Error ; err != nil {
2021-10-05 18:42:39 +02:00
log . Errorf ( "file: %s while saving" , err )
2020-07-07 10:51:55 +02:00
return err
}
2021-09-22 19:33:41 +02:00
if _ , err := m . SaveMarkers ( ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while saving markers" , clean . Log ( m . FileUID ) , err )
2021-05-26 14:41:59 +02:00
return err
}
2022-01-06 14:33:49 +01:00
return m . ResolvePrimary ( )
2020-07-06 19:15:57 +02:00
}
2022-01-06 14:33:49 +01:00
// ResolvePrimary ensures there is only one primary file for a photo.
2022-03-30 20:36:25 +02:00
func ( m * File ) ResolvePrimary ( ) ( err error ) {
filePrimaryMutex . Lock ( )
defer filePrimaryMutex . Unlock ( )
2022-01-06 14:33:49 +01:00
2022-03-30 20:36:25 +02:00
if ! m . FilePrimary {
return nil
2020-12-11 17:21:13 +01:00
}
2022-03-30 20:36:25 +02:00
err = UnscopedDb ( ) .
Exec ( "UPDATE files SET file_primary = (id = ?) WHERE photo_id = ?" , m . ID , m . PhotoID ) . Error
if err == nil {
m . RegenerateIndex ( )
}
return err
2020-12-11 17:21:13 +01:00
}
2022-10-02 11:38:30 +02:00
// Save updates the record in the database or inserts a new record if it does not already exist.
2020-05-13 15:36:42 +02:00
func ( m * File ) Save ( ) error {
2020-05-18 17:24:54 +02:00
if m . PhotoID == 0 {
2022-01-05 11:40:44 +01:00
return fmt . Errorf ( "file %s: cannot save file with empty photo id" , m . FileUID )
2020-07-07 10:51:55 +02:00
}
if err := UnscopedDb ( ) . Save ( m ) . Error ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while saving" , clean . Log ( m . FileUID ) , err )
2020-07-07 10:51:55 +02:00
return err
2020-05-18 17:24:54 +02:00
}
2021-09-22 19:33:41 +02:00
if _ , err := m . SaveMarkers ( ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "file %s: %s while saving markers" , clean . Log ( m . FileUID ) , err )
2021-05-26 14:41:59 +02:00
return err
}
2020-12-11 17:21:13 +01:00
return m . ResolvePrimary ( )
2020-05-13 15:36:42 +02:00
}
2020-05-16 17:07:44 +02:00
2023-09-03 16:44:30 +02:00
// UpdateVideoInfos updated related video files so they are properly grouped with the primary image in search results.
2023-08-18 09:12:38 +02:00
// see https://github.com/photoprism/photoprism/pull/3588#issuecomment-1683429455
2020-05-16 17:07:44 +02:00
func ( m * File ) UpdateVideoInfos ( ) error {
2023-08-15 14:51:32 +02:00
if m . PhotoID <= 0 {
return fmt . Errorf ( "file has invalid photo id" )
}
2023-08-18 09:12:38 +02:00
// Set the video dimensions from the primary image if it could not be determined from the video metadata.
// see https://github.com/photoprism/photoprism/blob/develop/internal/photoprism/index_mediafile.go
dimensions := FileDimensions { }
2020-05-16 17:07:44 +02:00
2023-08-15 14:51:32 +02:00
if err := deepcopier . Copy ( & dimensions ) . From ( m ) ; err != nil {
return err
} else if err = Db ( ) . Model ( File { } ) . Where ( "photo_id = ? AND file_video = 1 AND file_width <= 0" , m . PhotoID ) . Updates ( dimensions ) . Error ; err != nil {
2020-05-16 17:07:44 +02:00
return err
}
2023-08-18 09:12:38 +02:00
// Set the video appearance from the primary file if it could not be detected e.g. from a JPEG sidecar file.
// see https://github.com/photoprism/photoprism/blob/develop/internal/photoprism/index_mediafile.go
appearance := FileAppearance { }
2023-08-15 14:51:32 +02:00
if err := deepcopier . Copy ( & appearance ) . From ( m ) ; err != nil {
return err
2023-09-03 16:44:30 +02:00
} else if err = Db ( ) . Model ( File { } ) . Where ( "photo_id = ? AND file_video = 1" , m . PhotoID ) . Updates ( appearance ) . Error ; err != nil {
2023-08-15 14:51:32 +02:00
return err
}
return nil
2020-05-16 17:07:44 +02:00
}
2020-05-25 19:10:44 +02:00
2021-05-26 14:41:59 +02:00
// Update updates a column in the database.
2020-05-25 19:10:44 +02:00
func ( m * File ) Update ( attr string , value interface { } ) error {
2022-04-04 08:54:03 +02:00
return UnscopedDb ( ) . Model ( m ) . UpdateColumn ( attr , value ) . Error
2020-05-25 19:10:44 +02:00
}
2020-11-15 15:15:56 +01:00
// Updates multiple columns in the database.
2020-11-20 17:25:46 +01:00
func ( m * File ) Updates ( values interface { } ) error {
2022-04-04 08:54:03 +02:00
return UnscopedDb ( ) . Model ( m ) . UpdateColumns ( values ) . Error
2020-11-15 15:15:56 +01:00
}
2020-11-21 15:43:13 +01:00
// Rename updates the name and path of this file.
2020-11-20 17:25:46 +01:00
func ( m * File ) Rename ( fileName , rootName , filePath , fileBase string ) error {
2022-04-15 09:42:07 +02:00
log . Debugf ( "file %s: renaming %s to %s" , clean . Log ( m . FileUID ) , clean . Log ( m . FileName ) , clean . Log ( fileName ) )
2020-11-21 18:33:19 +01:00
2020-11-21 15:43:13 +01:00
// Update database row.
2020-11-20 17:25:46 +01:00
if err := m . Updates ( map [ string ] interface { } {
2020-11-21 17:36:41 +01:00
"FileName" : fileName ,
"FileRoot" : rootName ,
2020-11-21 15:43:13 +01:00
"FileMissing" : false ,
2020-11-21 17:36:41 +01:00
"DeletedAt" : nil ,
} ) ; err != nil {
2020-11-20 17:25:46 +01:00
return err
}
2020-11-21 15:43:13 +01:00
m . FileName = fileName
m . FileRoot = rootName
m . FileMissing = false
m . DeletedAt = nil
2020-11-20 17:25:46 +01:00
// Update photo path and name if possible.
if p := m . RelatedPhoto ( ) ; p != nil {
return p . Updates ( map [ string ] interface { } {
"PhotoPath" : filePath ,
"PhotoName" : fileBase ,
} )
}
return nil
2020-11-15 15:15:56 +01:00
}
2020-11-21 15:43:13 +01:00
// Undelete removes the missing flag from this file.
func ( m * File ) Undelete ( ) error {
if ! m . Missing ( ) {
return nil
}
// Update database row.
err := m . Updates ( map [ string ] interface { } {
"FileMissing" : false ,
2020-11-21 17:36:41 +01:00
"DeletedAt" : nil ,
2020-11-21 15:43:13 +01:00
} )
if err != nil {
return err
}
2022-04-15 09:42:07 +02:00
log . Debugf ( "file %s: removed missing flag from %s" , clean . Log ( m . FileUID ) , clean . Log ( m . FileName ) )
2020-11-21 15:43:13 +01:00
m . FileMissing = false
m . DeletedAt = nil
return nil
}
2020-05-25 19:10:44 +02:00
// RelatedPhoto returns the related photo entity.
func ( m * File ) RelatedPhoto ( ) * Photo {
if m . Photo != nil {
return m . Photo
}
photo := Photo { }
UnscopedDb ( ) . Model ( m ) . Related ( & photo )
return & photo
}
2023-02-11 20:18:04 +01:00
// NoJPEG returns true if the file is not a JPEG image.
2020-05-25 19:10:44 +02:00
func ( m * File ) NoJPEG ( ) bool {
2022-04-15 09:42:07 +02:00
return fs . ImageJPEG . NotEqual ( m . FileType )
2020-05-25 19:10:44 +02:00
}
2020-06-22 15:16:26 +02:00
2023-02-11 20:18:04 +01:00
// NoPNG returns true if the file is not a PNG image.
func ( m * File ) NoPNG ( ) bool {
return fs . ImagePNG . NotEqual ( m . FileType )
}
2023-02-22 20:58:21 +01:00
// Type returns the file type.
func ( m * File ) Type ( ) fs . Type {
return fs . Type ( m . FileType )
}
2020-06-22 15:16:26 +02:00
// Links returns all share links for this entity.
func ( m * File ) Links ( ) Links {
return FindLinks ( "" , m . FileUID )
}
2020-07-16 13:02:48 +02:00
2022-04-14 10:49:56 +02:00
// Panorama checks if the file appears to be a panoramic image.
2020-07-16 13:02:48 +02:00
func ( m * File ) Panorama ( ) bool {
if m . FileSidecar || m . FileWidth <= 1000 || m . FileHeight <= 500 {
2022-04-14 10:49:56 +02:00
// Too small.
2020-07-16 13:02:48 +02:00
return false
2022-04-15 09:42:07 +02:00
} else if m . Projection ( ) != projection . Unknown {
2022-04-14 10:49:56 +02:00
// Panoramic projection.
return true
2020-07-16 13:02:48 +02:00
}
2022-04-14 10:49:56 +02:00
// Decide based on aspect ratio.
return float64 ( m . FileWidth ) / float64 ( m . FileHeight ) > 1.9
2021-09-03 19:02:26 +02:00
}
2022-07-06 23:01:54 +02:00
// Bounds returns the file dimensions as image.Rectangle.
func ( m * File ) Bounds ( ) image . Rectangle {
return image . Rectangle { Min : image . Point { } , Max : image . Point { X : m . FileWidth , Y : m . FileHeight } }
}
2021-12-09 07:00:39 +01:00
// Projection returns the panorama projection name if any.
2022-04-15 09:42:07 +02:00
func ( m * File ) Projection ( ) projection . Type {
return projection . New ( m . FileProjection )
2021-09-03 19:02:26 +02:00
}
2021-12-09 07:00:39 +01:00
// SetProjection sets the panorama projection name.
2022-04-15 09:42:07 +02:00
func ( m * File ) SetProjection ( s string ) {
if s == "" {
return
} else if t := projection . New ( s ) ; ! t . Unknown ( ) {
m . FileProjection = t . String ( )
}
2021-12-09 07:00:39 +01:00
}
2022-01-05 16:37:19 +01:00
// IsHDR returns true if it is a high dynamic range file.
func ( m * File ) IsHDR ( ) bool {
return m . FileHDR
}
// SetHDR sets the high dynamic range flag.
func ( m * File ) SetHDR ( isHdr bool ) {
if isHdr {
m . FileHDR = true
}
}
// ResetHDR removes the high dynamic range flag.
func ( m * File ) ResetHDR ( ) {
m . FileHDR = false
}
2022-04-13 22:17:59 +02:00
// HasWatermark returns true if the file has a watermark.
func ( m * File ) HasWatermark ( ) bool {
return m . FileWatermark
}
// IsAnimated returns true if the file has animated image frames.
func ( m * File ) IsAnimated ( ) bool {
2023-02-22 20:58:21 +01:00
return ( m . FileFrames > 1 || m . FileDuration > 0 ) && media . Image . Equal ( m . MediaType )
2022-04-13 22:17:59 +02:00
}
2021-12-09 07:00:39 +01:00
// ColorProfile returns the ICC color profile name if any.
func ( m * File ) ColorProfile ( ) string {
2022-03-30 20:36:25 +02:00
return SanitizeStringType ( m . FileColorProfile )
2021-12-09 07:00:39 +01:00
}
// HasColorProfile tests if the file has a matching color profile.
func ( m * File ) HasColorProfile ( profile colors . Profile ) bool {
return profile . Equal ( m . FileColorProfile )
}
// SetColorProfile sets the ICC color profile name such as "Display P3".
func ( m * File ) SetColorProfile ( name string ) {
2022-03-30 20:36:25 +02:00
if name = SanitizeStringType ( name ) ; name != "" {
m . FileColorProfile = SanitizeStringType ( name )
2022-01-03 17:29:43 +01:00
}
}
// ResetColorProfile removes the ICC color profile name.
func ( m * File ) ResetColorProfile ( ) {
m . FileColorProfile = ""
2020-07-16 13:02:48 +02:00
}
2021-05-26 14:41:59 +02:00
2022-04-13 22:17:59 +02:00
// SetSoftware sets the software name.
func ( m * File ) SetSoftware ( name string ) {
if name = SanitizeStringType ( name ) ; name != "" {
m . FileSoftware = name
}
}
// SetDuration sets the video/animation duration.
func ( m * File ) SetDuration ( d time . Duration ) {
if d <= 0 {
return
}
2023-02-22 16:33:33 +01:00
m . FileDuration = d . Round ( 10e6 )
2022-04-13 22:17:59 +02:00
// Update number of frames.
2022-04-15 09:42:07 +02:00
if m . FileFrames == 0 && m . FileFPS > 1 {
2022-04-13 22:17:59 +02:00
m . FileFrames = int ( math . Round ( m . FileFPS * m . FileDuration . Seconds ( ) ) )
}
// Update number of frames per second.
2022-04-15 09:42:07 +02:00
if m . FileFPS == 0 && m . FileFrames > 1 {
2022-04-13 22:17:59 +02:00
m . FileFPS = float64 ( m . FileFrames ) / m . FileDuration . Seconds ( )
}
}
2022-06-26 19:47:12 +02:00
// Bitrate returns the average bitrate in MBit/s if the file has a duration.
func ( m * File ) Bitrate ( ) float64 {
2023-09-24 17:13:06 +02:00
// Return 0 if file size or video duration are unknown.
2022-06-26 19:47:12 +02:00
if m . FileSize <= 0 || m . FileDuration <= 0 {
return 0
}
// Divide number of bits through the duration in seconds.
return ( ( float64 ( m . FileSize ) * 8 ) / m . FileDuration . Seconds ( ) ) / 1e6
}
2022-04-13 22:17:59 +02:00
// SetFPS sets the average number of frames per second.
func ( m * File ) SetFPS ( frameRate float64 ) {
if frameRate <= 0 {
return
}
m . FileFPS = frameRate
// Update number of frames.
2022-04-15 09:42:07 +02:00
if m . FileFrames == 0 && m . FileDuration > time . Second {
2022-04-13 22:17:59 +02:00
m . FileFrames = int ( math . Round ( m . FileFPS * m . FileDuration . Seconds ( ) ) )
}
}
// SetFrames sets the number of video/animation frames.
func ( m * File ) SetFrames ( n int ) {
if n <= 0 {
return
}
m . FileFrames = n
// Update FPS.
if m . FileFPS <= 0 && m . FileDuration > 0 {
m . FileFPS = float64 ( m . FileFrames ) / m . FileDuration . Seconds ( )
2023-02-14 14:43:49 +01:00
} else if m . FileFPS == 0 && m . FileDuration == 0 {
m . FileFPS = 30.0 // Assume 30 frames per second.
m . FileDuration = time . Duration ( float64 ( m . FileFrames ) / m . FileFPS ) * time . Second
2022-04-13 22:17:59 +02:00
}
}
2022-04-15 09:42:07 +02:00
// SetMediaUTC sets the media creation date from metadata as unix time in ms.
func ( m * File ) SetMediaUTC ( taken time . Time ) {
2022-04-13 22:17:59 +02:00
if taken . IsZero ( ) {
return
}
2022-04-15 09:42:07 +02:00
m . MediaUTC = taken . UTC ( ) . UnixMilli ( )
2022-04-13 22:17:59 +02:00
}
2021-05-26 14:41:59 +02:00
// AddFaces adds face markers to the file.
func ( m * File ) AddFaces ( faces face . Faces ) {
2021-09-06 05:13:53 +02:00
sort . Slice ( faces , func ( i , j int ) bool {
return faces [ i ] . Size ( ) > faces [ j ] . Size ( )
} )
2021-05-26 14:41:59 +02:00
for _ , f := range faces {
m . AddFace ( f , "" )
}
}
// AddFace adds a face marker to the file.
2022-10-02 11:38:30 +02:00
func ( m * File ) AddFace ( f face . Face , subjUid string ) {
2021-09-30 13:44:23 +02:00
// Only add faces with exactly one embedding so that they can be compared and clustered.
if ! f . Embeddings . One ( ) {
2021-09-22 19:33:41 +02:00
return
}
// Create new marker from face.
2022-10-02 11:38:30 +02:00
marker := NewFaceMarker ( f , * m , subjUid )
2021-09-02 11:12:42 +02:00
2022-10-31 13:25:02 +01:00
// Failed creating new marker?
2021-09-22 19:33:41 +02:00
if marker == nil {
return
}
// Append marker if it doesn't conflict with existing marker.
if markers := m . Markers ( ) ; ! markers . Contains ( * marker ) {
2021-09-30 13:44:23 +02:00
markers . AppendWithEmbedding ( * marker )
2021-05-31 15:40:52 +02:00
}
}
2021-09-23 23:46:17 +02:00
// ValidFaceCount returns the number of valid face markers.
func ( m * File ) ValidFaceCount ( ) ( c int ) {
return ValidFaceCount ( m . FileUID )
2021-09-22 19:33:41 +02:00
}
// UpdatePhotoFaceCount updates the faces count in the index and returns it if the file is primary.
func ( m * File ) UpdatePhotoFaceCount ( ) ( c int , err error ) {
2023-02-11 20:18:04 +01:00
// Previews file of an existing photo?
2021-09-22 19:33:41 +02:00
if ! m . FilePrimary || m . PhotoID == 0 {
return 0 , nil
2021-06-02 17:25:04 +02:00
}
2021-09-22 19:33:41 +02:00
2021-09-23 23:46:17 +02:00
c = m . ValidFaceCount ( )
2021-09-22 19:33:41 +02:00
err = UnscopedDb ( ) . Model ( Photo { } ) .
Where ( "id = ?" , m . PhotoID ) .
2022-04-04 08:54:03 +02:00
UpdateColumn ( "photo_faces" , c ) . Error
2021-09-22 19:33:41 +02:00
return c , err
}
// SaveMarkers updates markers in the index.
func ( m * File ) SaveMarkers ( ) ( count int , err error ) {
if m . markers == nil {
return 0 , nil
}
return m . markers . Save ( m )
2021-06-02 17:25:04 +02:00
}
2021-09-02 11:12:42 +02:00
// Markers finds and returns existing file markers.
func ( m * File ) Markers ( ) * Markers {
if m . markers != nil {
return m . markers
2021-09-22 19:33:41 +02:00
} else if m . FileUID == "" {
m . markers = & Markers { }
} else if res , err := FindMarkers ( m . FileUID ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Warnf ( "file %s: %s while loading markers" , clean . Log ( m . FileUID ) , err )
2021-09-02 11:12:42 +02:00
m . markers = & Markers { }
2021-05-31 15:40:52 +02:00
} else {
2021-09-02 11:12:42 +02:00
m . markers = & res
2021-05-31 15:40:52 +02:00
}
2021-09-02 11:12:42 +02:00
return m . markers
}
2021-09-23 23:46:17 +02:00
// UnsavedMarkers tests if any marker hasn't been saved yet.
func ( m * File ) UnsavedMarkers ( ) bool {
if m . markers == nil {
return false
}
return m . markers . Unsaved ( )
}
2021-09-02 11:12:42 +02:00
// SubjectNames returns all known subject names.
func ( m * File ) SubjectNames ( ) [ ] string {
return m . Markers ( ) . SubjectNames ( )
2021-05-26 14:41:59 +02:00
}
2023-03-20 16:18:27 +01:00
// Orientation returns the file's Exif orientation value.
func ( m * File ) Orientation ( ) int {
return clean . Orientation ( m . FileOrientation )
}
// SetOrientation sets the file's Exif orientation value.
func ( m * File ) SetOrientation ( val int , src string ) * File {
// Ignore invalid values.
val = clean . Orientation ( val )
if val == 0 {
return m
}
// Only set values with a matching or higher priority.
if SrcPriority [ src ] >= SrcPriority [ m . FileOrientationSrc ] {
m . FileOrientation = val
m . FileOrientationSrc = src
}
return m
}