photoprism/internal/query/photo.go
Michael Mayer 82d61d1f93 File Types: Add experimental support for animated GIFs #590 #2207
Animated GIFs are transcoded to AVC because it is much smaller and
thus also suitable for long/large animations. In addition, this commit
adds support for more metadata fields such as frame rate, number of
frames, file capture timestamp (unix milliseconds), media type,
and software version. Support for SVG files can later be implemented in
a similar way.
2022-04-13 22:17:59 +02:00

190 lines
5.3 KiB
Go

package query
import (
"time"
"github.com/dustin/go-humanize/english"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
)
// PhotoByID returns a Photo based on the ID.
func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("id = ?", photoID).
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}).
Preload("Labels.Label").
Preload("Camera").
Preload("Lens").
Preload("Details").
Preload("Place").
Preload("Cell").
Preload("Cell.Place").
First(&photo).Error; err != nil {
return photo, err
}
return photo, nil
}
// PhotoByUID returns a Photo based on the UID.
func PhotoByUID(photoUID string) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("photo_uid = ?", photoUID).
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}).
Preload("Labels.Label").
Preload("Camera").
Preload("Lens").
Preload("Details").
Preload("Place").
Preload("Cell").
Preload("Cell.Place").
First(&photo).Error; err != nil {
return photo, err
}
return photo, nil
}
// PhotoPreloadByUID returns a Photo based on the UID with all dependencies preloaded.
func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("photo_uid = ?", photoUID).
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}).
Preload("Labels.Label").
Preload("Camera").
Preload("Lens").
Preload("Details").
Preload("Place").
Preload("Cell").
Preload("Cell.Place").
First(&photo).Error; err != nil {
return photo, err
}
photo.PreloadMany()
return photo, nil
}
// PhotosMissing returns photo entities without existing files.
func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
err = Db().
Select("photos.*").
Where("id NOT IN (SELECT photo_id FROM files WHERE file_missing = 0 AND file_root = '/' AND deleted_at IS NULL)").
Where("photos.photo_type <> ?", entity.TypeMeta).
Group("photos.id").
Limit(limit).Offset(offset).Find(&entities).Error
return entities, err
}
// PhotosMetadataUpdate returns photos selected for metadata maintenance.
func PhotosMetadataUpdate(limit, offset int, delay, interval time.Duration) (entities entity.Photos, err error) {
err = Db().
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}).
Preload("Labels.Label").
Preload("Camera").
Preload("Lens").
Preload("Details").
Preload("Place").
Preload("Cell").
Preload("Cell.Place").
Where("checked_at IS NULL OR checked_at < ?", time.Now().Add(-1*interval)).
Where("updated_at < ? OR (cell_id = 'zz' AND photo_lat <> 0)", time.Now().Add(-1*delay)).
Order("photos.ID ASC").Limit(limit).Offset(offset).Find(&entities).Error
return entities, err
}
// OrphanPhotos finds orphan index entries that may be removed.
func OrphanPhotos() (photos entity.Photos, err error) {
err = UnscopedDb().
Raw(`SELECT * FROM photos WHERE
deleted_at IS NOT NULL
AND photo_quality = -1
AND id NOT IN (SELECT photo_id FROM files WHERE files.deleted_at IS NULL)`).
Find(&photos).Error
return photos, err
}
// FixPrimaries tries to set a primary file for photos that have none.
func FixPrimaries() error {
mutex.Index.Lock()
defer mutex.Index.Unlock()
start := time.Now()
var photos entity.Photos
// Remove primary file flag from broken or missing files.
if err := UnscopedDb().Table(entity.File{}.TableName()).
Where("file_error <> '' OR file_missing = 1").
UpdateColumn("file_primary", 0).Error; err != nil {
return err
}
// Find photos without primary file.
if err := UnscopedDb().
Raw(`SELECT * FROM photos
WHERE deleted_at IS NULL
AND id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1)`).
Find(&photos).Error; err != nil {
return err
}
if len(photos) == 0 {
log.Debugf("index: found no photos without primary file [%s]", time.Since(start))
return nil
}
// Try to find matching primary files.
for _, p := range photos {
log.Debugf("index: searching primary file for %s", p.PhotoUID)
if err := p.SetPrimary(""); err != nil {
log.Infof("index: %s (set primary)", err)
}
}
log.Debugf("index: updated primary files [%s]", time.Since(start))
return nil
}
// FlagHiddenPhotos sets the quality score of photos without valid primary file to -1.
func FlagHiddenPhotos() error {
mutex.Index.Lock()
defer mutex.Index.Unlock()
start := time.Now()
res := Db().Table("photos").
Where("id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND file_missing = 0 AND file_error = '' AND deleted_at IS NULL)").
Update("photo_quality", -1)
switch DbDialect() {
case MySQL:
if res.RowsAffected > 0 {
log.Infof("index: flagged %s as hidden or missing [%s]", english.Plural(int(res.RowsAffected), "photo", "photos"), time.Since(start))
}
case SQLite3:
if res.RowsAffected > 0 {
log.Debugf("index: flagged %s as hidden or missing [%s]", english.Plural(int(res.RowsAffected), "photo", "photos"), time.Since(start))
}
default:
log.Warnf("sql: unsupported dialect %s", DbDialect())
return nil
}
return res.Error
}