2019-12-11 16:55:18 +01:00
package entity
2018-07-18 15:17:56 +02:00
import (
2020-06-01 09:45:24 +02:00
"fmt"
2019-06-18 06:37:10 +02:00
"strings"
2019-12-04 12:11:11 +01:00
"time"
2019-06-18 06:37:10 +02:00
"github.com/gosimple/slug"
2018-07-18 15:17:56 +02:00
"github.com/jinzhu/gorm"
2020-05-30 01:41:47 +02:00
"github.com/photoprism/photoprism/internal/event"
2020-04-20 10:38:01 +02:00
"github.com/photoprism/photoprism/internal/form"
2020-01-12 14:00:56 +01:00
"github.com/photoprism/photoprism/pkg/rnd"
2020-04-26 14:31:33 +02:00
"github.com/photoprism/photoprism/pkg/txt"
2020-04-20 10:38:01 +02:00
"github.com/ulule/deepcopier"
2018-07-18 15:17:56 +02:00
)
2020-06-08 18:32:51 +02:00
const (
AlbumDefault = "album"
AlbumFolder = "folder"
AlbumMoment = "moment"
AlbumMonth = "month"
AlbumState = "state"
)
2020-06-01 09:45:24 +02:00
type Albums [ ] Album
2020-02-21 01:14:45 +01:00
// Album represents a photo album
2018-07-18 15:17:56 +02:00
type Album struct {
2020-05-23 20:58:58 +02:00
ID uint ` gorm:"primary_key" json:"ID" yaml:"-" `
2020-09-13 17:51:43 +02:00
AlbumUID string ` gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID" `
CoverUID string ` gorm:"type:VARBINARY(42);" json:"CoverUID" yaml:"CoverUID,omitempty" `
FolderUID string ` gorm:"type:VARBINARY(42);index;" json:"FolderUID" yaml:"FolderUID,omitempty" `
AlbumSlug string ` gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"Slug" `
2020-12-08 22:40:13 +01:00
AlbumPath string ` gorm:"type:VARBINARY(768);index;" json:"Path" yaml:"-" `
2020-09-13 17:51:43 +02:00
AlbumType string ` gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty" `
AlbumTitle string ` gorm:"type:VARCHAR(255);" json:"Title" yaml:"Title" `
AlbumLocation string ` gorm:"type:VARCHAR(255);" json:"Location" yaml:"Location,omitempty" `
AlbumCategory string ` gorm:"type:VARCHAR(255);index;" json:"Category" yaml:"Category,omitempty" `
AlbumCaption string ` gorm:"type:TEXT;" json:"Caption" yaml:"Caption,omitempty" `
AlbumDescription string ` gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty" `
AlbumNotes string ` gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty" `
AlbumFilter string ` gorm:"type:VARBINARY(1024);" json:"Filter" yaml:"Filter,omitempty" `
AlbumOrder string ` gorm:"type:VARBINARY(32);" json:"Order" yaml:"Order,omitempty" `
AlbumTemplate string ` gorm:"type:VARBINARY(255);" json:"Template" yaml:"Template,omitempty" `
AlbumCountry string ` gorm:"type:VARBINARY(2);index:idx_albums_country_year_month;default:'zz'" json:"Country" yaml:"Country,omitempty" `
2020-05-26 09:02:19 +02:00
AlbumYear int ` gorm:"index:idx_albums_country_year_month;" json:"Year" yaml:"Year,omitempty" `
AlbumMonth int ` gorm:"index:idx_albums_country_year_month;" json:"Month" yaml:"Month,omitempty" `
2020-07-06 07:41:33 +02:00
AlbumDay int ` json:"Day" yaml:"Day,omitempty" `
2020-05-23 20:58:58 +02:00
AlbumFavorite bool ` json:"Favorite" yaml:"Favorite,omitempty" `
2020-05-26 09:02:19 +02:00
AlbumPrivate bool ` json:"Private" yaml:"Private,omitempty" `
2020-05-23 20:58:58 +02:00
CreatedAt time . Time ` json:"CreatedAt" yaml:"-" `
UpdatedAt time . Time ` json:"UpdatedAt" yaml:"-" `
DeletedAt * time . Time ` sql:"index" json:"-" yaml:"-" `
2018-07-18 15:17:56 +02:00
}
2019-06-04 18:26:35 +02:00
2020-06-01 09:45:24 +02:00
// AddPhotoToAlbums adds a photo UID to multiple albums and automatically creates them if needed.
func AddPhotoToAlbums ( photo string , albums [ ] string ) ( err error ) {
if photo == "" || len ( albums ) == 0 {
// Do nothing.
2020-05-01 12:57:26 +02:00
return nil
2019-12-06 10:26:57 +01:00
}
2020-06-01 09:45:24 +02:00
if ! rnd . IsPPID ( photo , 'p' ) {
return fmt . Errorf ( "album: invalid photo uid %s" , photo )
}
for _ , album := range albums {
var aUID string
if album == "" {
log . Debugf ( "album: empty album identifier while adding photo %s" , photo )
continue
}
if rnd . IsPPID ( album , 'a' ) {
aUID = album
} else {
2020-06-08 18:32:51 +02:00
a := NewAlbum ( album , AlbumDefault )
2020-06-01 09:45:24 +02:00
if err = a . Find ( ) ; err == nil {
aUID = a . AlbumUID
} else if err = a . Create ( ) ; err == nil {
aUID = a . AlbumUID
} else {
log . Errorf ( "album: %s (add photo %s to albums)" , err . Error ( ) , photo )
}
}
if aUID != "" {
entry := PhotoAlbum { AlbumUID : aUID , PhotoUID : photo , Hidden : false }
if err = entry . Save ( ) ; err != nil {
log . Errorf ( "album: %s (add photo %s to albums)" , err . Error ( ) , photo )
}
}
}
return err
2019-06-04 18:26:35 +02:00
}
2019-06-18 06:37:10 +02:00
2020-02-21 01:14:45 +01:00
// NewAlbum creates a new album; default name is current month and year
2020-05-26 09:02:19 +02:00
func NewAlbum ( albumTitle , albumType string ) * Album {
2020-06-26 12:16:13 +02:00
now := Timestamp ( )
2019-06-18 06:37:10 +02:00
2020-05-29 12:21:17 +02:00
if albumType == "" {
2020-06-08 18:32:51 +02:00
albumType = AlbumDefault
2020-05-29 12:21:17 +02:00
}
2019-06-18 06:37:10 +02:00
result := & Album {
2020-04-20 12:53:58 +02:00
AlbumOrder : SortOrderOldest ,
2020-05-23 20:58:58 +02:00
AlbumType : albumType ,
2020-04-26 14:31:33 +02:00
CreatedAt : now ,
UpdatedAt : now ,
2019-06-18 06:37:10 +02:00
}
2020-05-26 09:02:19 +02:00
result . SetTitle ( albumTitle )
2020-04-26 14:31:33 +02:00
2019-06-18 06:37:10 +02:00
return result
}
2019-12-03 23:55:24 +01:00
2020-05-30 15:42:04 +02:00
// NewFolderAlbum creates a new folder album.
2020-12-09 00:44:33 +01:00
func NewFolderAlbum ( albumTitle , albumPath , albumFilter string ) * Album {
if albumTitle == "" || albumPath == "" || albumFilter == "" {
2020-05-30 15:42:04 +02:00
return nil
}
2020-06-26 12:16:13 +02:00
now := Timestamp ( )
2020-05-30 15:42:04 +02:00
result := & Album {
2020-06-30 11:27:02 +02:00
AlbumOrder : SortOrderAdded ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumFolder ,
2020-05-30 15:42:04 +02:00
AlbumTitle : albumTitle ,
2020-12-09 00:44:33 +01:00
AlbumSlug : slug . Make ( albumPath ) ,
2020-12-08 19:47:27 +01:00
AlbumPath : albumPath ,
2020-05-30 15:42:04 +02:00
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
return result
}
// NewMomentsAlbum creates a new moment.
func NewMomentsAlbum ( albumTitle , albumSlug , albumFilter string ) * Album {
2020-05-30 01:41:47 +02:00
if albumTitle == "" || albumSlug == "" || albumFilter == "" {
return nil
}
2020-06-26 12:16:13 +02:00
now := Timestamp ( )
2020-05-30 01:41:47 +02:00
result := & Album {
AlbumOrder : SortOrderOldest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumMoment ,
AlbumTitle : albumTitle ,
AlbumSlug : albumSlug ,
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
return result
}
// NewStateAlbum creates a new moment.
func NewStateAlbum ( albumTitle , albumSlug , albumFilter string ) * Album {
if albumTitle == "" || albumSlug == "" || albumFilter == "" {
return nil
}
2020-06-26 12:16:13 +02:00
now := Timestamp ( )
2020-06-08 18:32:51 +02:00
result := & Album {
2020-06-30 11:27:02 +02:00
AlbumOrder : SortOrderNewest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumState ,
2020-05-30 01:41:47 +02:00
AlbumTitle : albumTitle ,
AlbumSlug : albumSlug ,
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
return result
}
2020-05-30 15:42:04 +02:00
// NewMonthAlbum creates a new month album.
func NewMonthAlbum ( albumTitle , albumSlug string , year , month int ) * Album {
2020-05-30 01:41:47 +02:00
if albumTitle == "" || albumSlug == "" || year == 0 || month == 0 {
return nil
}
f := form . PhotoSearch {
2020-06-05 10:59:59 +02:00
Year : year ,
Month : month ,
Public : true ,
2020-05-30 01:41:47 +02:00
}
2020-06-26 12:16:13 +02:00
now := Timestamp ( )
2020-05-30 01:41:47 +02:00
result := & Album {
AlbumOrder : SortOrderOldest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumMonth ,
2020-05-30 01:41:47 +02:00
AlbumTitle : albumTitle ,
AlbumSlug : albumSlug ,
AlbumFilter : f . Serialize ( ) ,
AlbumYear : year ,
AlbumMonth : month ,
CreatedAt : now ,
UpdatedAt : now ,
}
return result
}
2020-06-01 09:45:24 +02:00
// FindAlbumBySlug finds a matching album or returns nil.
2020-12-08 22:40:13 +01:00
func FindAlbumBySlug ( albumSlug , albumType string ) * Album {
2020-06-01 09:45:24 +02:00
result := Album { }
2020-12-08 22:40:13 +01:00
if err := UnscopedDb ( ) . Where ( "album_slug = ? AND album_type = ?" , albumSlug , albumType ) . First ( & result ) . Error ; err != nil {
return nil
}
return & result
}
// FindFolderAlbum finds a matching folder album or returns nil.
func FindFolderAlbum ( albumSlug , albumPath string ) * Album {
result := Album { }
if err := UnscopedDb ( ) . Where ( "((album_slug <> '' AND album_slug = ?) OR album_path = ?) AND album_type = ?" , albumSlug , albumPath , AlbumFolder ) . First ( & result ) . Error ; err != nil {
2020-06-01 09:45:24 +02:00
return nil
}
return & result
}
2020-06-25 01:20:58 +02:00
// Find returns an entity from the database.
2020-06-01 09:45:24 +02:00
func ( m * Album ) Find ( ) error {
if rnd . IsPPID ( m . AlbumUID , 'a' ) {
if err := UnscopedDb ( ) . First ( m , "album_uid = ?" , m . AlbumUID ) . Error ; err != nil {
return err
}
}
if err := UnscopedDb ( ) . First ( m , "album_slug = ? AND album_type = ?" , m . AlbumSlug , m . AlbumType ) . Error ; err != nil {
return err
}
return nil
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func ( m * Album ) BeforeCreate ( scope * gorm . Scope ) error {
if rnd . IsUID ( m . AlbumUID , 'a' ) {
return nil
}
return scope . SetColumn ( "AlbumUID" , rnd . PPID ( 'a' ) )
}
// String returns the id or name as string.
func ( m * Album ) String ( ) string {
if m . AlbumSlug != "" {
return m . AlbumSlug
}
if m . AlbumTitle != "" {
return txt . Quote ( m . AlbumTitle )
}
if m . AlbumUID != "" {
return m . AlbumUID
}
return "[unknown album]"
}
2020-05-30 01:41:47 +02:00
// Checks if the album is of type moment.
func ( m * Album ) IsMoment ( ) bool {
2020-06-08 18:32:51 +02:00
return m . AlbumType == AlbumMoment
2020-05-30 01:41:47 +02:00
}
2020-05-26 09:02:19 +02:00
// SetTitle changes the album name.
func ( m * Album ) SetTitle ( title string ) {
title = strings . TrimSpace ( title )
2020-04-26 14:31:33 +02:00
2020-05-26 09:02:19 +02:00
if title == "" {
title = m . CreatedAt . Format ( "January 2006" )
2019-12-04 12:11:11 +01:00
}
2020-05-26 09:02:19 +02:00
m . AlbumTitle = txt . Clip ( title , txt . ClipDefault )
2020-04-26 14:31:33 +02:00
2020-06-08 18:32:51 +02:00
if m . AlbumType == AlbumDefault {
2020-05-30 21:11:56 +02:00
if len ( m . AlbumTitle ) < txt . ClipSlug {
m . AlbumSlug = slug . Make ( m . AlbumTitle )
} else {
m . AlbumSlug = slug . Make ( txt . Clip ( m . AlbumTitle , txt . ClipSlug ) ) + "-" + m . AlbumUID
}
2020-04-26 14:31:33 +02:00
}
2019-12-03 23:55:24 +01:00
}
2020-04-20 10:38:01 +02:00
2020-05-22 16:29:12 +02:00
// Saves the entity using form data and stores it in the database.
2020-05-26 11:00:39 +02:00
func ( m * Album ) SaveForm ( f form . Album ) error {
2020-04-20 10:38:01 +02:00
if err := deepcopier . Copy ( m ) . From ( f ) ; err != nil {
return err
}
2020-05-30 21:11:56 +02:00
if f . AlbumCategory != "" {
m . AlbumCategory = txt . Title ( txt . Clip ( f . AlbumCategory , txt . ClipKeyword ) )
}
2020-05-26 09:02:19 +02:00
if f . AlbumTitle != "" {
m . SetTitle ( f . AlbumTitle )
2020-04-20 10:38:01 +02:00
}
2020-04-30 20:07:03 +02:00
return Db ( ) . Save ( m ) . Error
2020-04-20 10:38:01 +02:00
}
2020-05-26 11:00:39 +02:00
// Updates a column in the database.
func ( m * Album ) Update ( attr string , value interface { } ) error {
return UnscopedDb ( ) . Model ( m ) . UpdateColumn ( attr , value ) . Error
}
2020-12-08 22:40:13 +01:00
// UpdatePath sets a unique path for an albums.
func ( m * Album ) UpdatePath ( albumPath string ) error {
2020-12-09 00:44:33 +01:00
if err := UnscopedDb ( ) . Model ( m ) . UpdateColumns ( map [ string ] interface { } {
"AlbumPath" : albumPath ,
"AlbumSlug" : slug . Make ( albumPath ) ,
} ) . Error ; err != nil {
2020-12-08 22:40:13 +01:00
return err
} else if err := UnscopedDb ( ) . Exec ( "UPDATE albums SET album_path = NULL WHERE album_path = ? AND id <> ?" , albumPath , m . ID ) . Error ; err != nil {
return err
}
return nil
}
2020-05-26 11:00:39 +02:00
// Save updates the existing or inserts a new row.
func ( m * Album ) Save ( ) error {
return Db ( ) . Save ( m ) . Error
}
// Create inserts a new row to the database.
func ( m * Album ) Create ( ) error {
2020-05-30 01:41:47 +02:00
if err := Db ( ) . Create ( m ) . Error ; err != nil {
return err
}
switch m . AlbumType {
2020-06-08 18:32:51 +02:00
case AlbumDefault :
2020-05-30 01:41:47 +02:00
event . Publish ( "count.albums" , event . Data { "count" : 1 } )
2020-06-08 18:32:51 +02:00
case AlbumMoment :
2020-05-30 01:41:47 +02:00
event . Publish ( "count.moments" , event . Data { "count" : 1 } )
2020-06-08 18:32:51 +02:00
case AlbumMonth :
2020-05-30 01:41:47 +02:00
event . Publish ( "count.months" , event . Data { "count" : 1 } )
2020-06-08 18:32:51 +02:00
case AlbumFolder :
2020-05-30 01:41:47 +02:00
event . Publish ( "count.folders" , event . Data { "count" : 1 } )
}
return nil
}
2020-06-01 09:45:24 +02:00
// Returns the album title.
func ( m * Album ) Title ( ) string {
return m . AlbumTitle
}
2020-05-30 01:41:47 +02:00
2020-06-01 09:45:24 +02:00
// AddPhotos adds photos to an existing album.
func ( m * Album ) AddPhotos ( UIDs [ ] string ) ( added [ ] PhotoAlbum ) {
for _ , uid := range UIDs {
entry := PhotoAlbum { AlbumUID : m . AlbumUID , PhotoUID : uid , Hidden : false }
if err := entry . Save ( ) ; err != nil {
log . Errorf ( "album: %s (add to album %s)" , err . Error ( ) , m )
} else {
added = append ( added , entry )
}
2020-05-30 01:41:47 +02:00
}
2020-06-01 09:45:24 +02:00
return added
}
// RemovePhotos removes photos from an album.
func ( m * Album ) RemovePhotos ( UIDs [ ] string ) ( removed [ ] PhotoAlbum ) {
for _ , uid := range UIDs {
entry := PhotoAlbum { AlbumUID : m . AlbumUID , PhotoUID : uid , Hidden : true }
if err := entry . Save ( ) ; err != nil {
log . Errorf ( "album: %s (remove from album %s)" , err . Error ( ) , m )
} else {
removed = append ( removed , entry )
}
}
return removed
2020-05-26 11:00:39 +02:00
}
2020-06-22 15:16:26 +02:00
// Links returns all share links for this entity.
func ( m * Album ) Links ( ) Links {
return FindLinks ( "" , m . AlbumUID )
}