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"
2020-12-14 20:37:54 +01:00
"os"
2021-09-20 23:32:35 +02:00
"strconv"
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
2018-07-18 15:17:56 +02:00
"github.com/jinzhu/gorm"
2021-11-29 15:04:13 +01:00
"github.com/ulule/deepcopier"
2021-11-20 16:36:34 +01:00
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"
2021-11-20 16:37:55 +01:00
"github.com/photoprism/photoprism/internal/maps"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/clean"
2020-01-12 14:00:56 +01:00
"github.com/photoprism/photoprism/pkg/rnd"
2023-01-30 12:27:34 +01:00
"github.com/photoprism/photoprism/pkg/sortby"
2020-04-26 14:31:33 +02:00
"github.com/photoprism/photoprism/pkg/txt"
2018-07-18 15:17:56 +02:00
)
2020-06-08 18:32:51 +02:00
const (
2023-04-01 14:25:05 +02:00
AlbumUID = byte ( 'a' )
AlbumManual = "album"
AlbumFolder = "folder"
AlbumMoment = "moment"
AlbumMonth = "month"
AlbumState = "state"
2020-06-08 18:32:51 +02:00
)
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-12-17 18:24:55 +01:00
ID uint ` gorm:"primary_key" json:"ID" yaml:"-" `
2022-10-02 11:38:30 +02:00
AlbumUID string ` gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID" `
ParentUID string ` gorm:"type:VARBINARY(42);default:'';" json:"ParentUID,omitempty" yaml:"ParentUID,omitempty" `
2021-09-23 23:46:17 +02:00
AlbumSlug string ` gorm:"type:VARBINARY(160);index;" json:"Slug" yaml:"Slug" `
2023-01-02 17:59:48 +01:00
AlbumPath string ` gorm:"type:VARCHAR(1024);index;" json:"Path,omitempty" yaml:"Path,omitempty" `
2020-12-17 18:24:55 +01:00
AlbumType string ` gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty" `
2021-09-23 23:46:17 +02:00
AlbumTitle string ` gorm:"type:VARCHAR(160);index;" json:"Title" yaml:"Title" `
AlbumLocation string ` gorm:"type:VARCHAR(160);" json:"Location" yaml:"Location,omitempty" `
AlbumCategory string ` gorm:"type:VARCHAR(100);index;" json:"Category" yaml:"Category,omitempty" `
2022-03-30 20:36:25 +02:00
AlbumCaption string ` gorm:"type:VARCHAR(1024);" json:"Caption" yaml:"Caption,omitempty" `
AlbumDescription string ` gorm:"type:VARCHAR(2048);" json:"Description" yaml:"Description,omitempty" `
AlbumNotes string ` gorm:"type:VARCHAR(1024);" json:"Notes" yaml:"Notes,omitempty" `
AlbumFilter string ` gorm:"type:VARBINARY(2048);" json:"Filter" yaml:"Filter,omitempty" `
2020-12-17 18:24:55 +01:00
AlbumOrder string ` gorm:"type:VARBINARY(32);" json:"Order" yaml:"Order,omitempty" `
AlbumTemplate string ` gorm:"type:VARBINARY(255);" json:"Template" yaml:"Template,omitempty" `
2021-11-18 13:13:48 +01:00
AlbumState string ` gorm:"type:VARCHAR(100);index;" json:"State" yaml:"State,omitempty" `
2021-09-23 17:05:56 +02:00
AlbumCountry string ` gorm:"type:VARBINARY(2);index:idx_albums_country_year_month;default:'zz';" json:"Country" yaml:"Country,omitempty" `
2021-09-18 20:41:30 +02:00
AlbumYear int ` gorm:"index:idx_albums_ymd;index:idx_albums_country_year_month;" json:"Year" yaml:"Year,omitempty" `
AlbumMonth int ` gorm:"index:idx_albums_ymd;index:idx_albums_country_year_month;" json:"Month" yaml:"Month,omitempty" `
2021-09-23 17:05:56 +02:00
AlbumDay int ` gorm:"index:idx_albums_ymd;" json:"Day" yaml:"Day,omitempty" `
2020-12-17 18:24:55 +01:00
AlbumFavorite bool ` json:"Favorite" yaml:"Favorite,omitempty" `
AlbumPrivate bool ` json:"Private" yaml:"Private,omitempty" `
2021-09-23 17:05:56 +02:00
Thumb string ` gorm:"type:VARBINARY(128);index;default:'';" json:"Thumb" yaml:"Thumb,omitempty" `
ThumbSrc string ` gorm:"type:VARBINARY(8);default:'';" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty" `
2022-10-02 11:38:30 +02:00
CreatedBy string ` gorm:"type:VARBINARY(42);index" json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty" `
2020-12-17 18:24:55 +01:00
CreatedAt time . Time ` json:"CreatedAt" yaml:"CreatedAt,omitempty" `
UpdatedAt time . Time ` json:"UpdatedAt" yaml:"UpdatedAt,omitempty" `
2022-10-02 11:38:30 +02:00
PublishedAt * time . Time ` sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty" `
2020-12-17 18:24:55 +01:00
DeletedAt * time . Time ` sql:"index" json:"DeletedAt" yaml:"DeletedAt,omitempty" `
2021-09-23 17:05:56 +02:00
Photos PhotoAlbums ` gorm:"foreignkey:AlbumUID;association_foreignkey:AlbumUID;" json:"-" yaml:"Photos,omitempty" `
2018-07-18 15:17:56 +02:00
}
2019-06-04 18:26:35 +02:00
2022-08-01 15:57:19 +02:00
// AfterUpdate flushes the album cache.
func ( m * Album ) AfterUpdate ( tx * gorm . DB ) ( err error ) {
FlushAlbumCache ( )
return
}
// AfterDelete flushes the album cache.
func ( m * Album ) AfterDelete ( tx * gorm . DB ) ( err error ) {
FlushAlbumCache ( )
return
}
2022-09-28 09:01:17 +02:00
// TableName returns the entity table name.
2021-08-30 18:58:27 +02:00
func ( Album ) TableName ( ) string {
return "albums"
}
2023-07-23 14:26:14 +02:00
// UpdateAlbum updates album attributes in the database.
func UpdateAlbum ( albumUID string , values interface { } ) ( err error ) {
if rnd . InvalidUID ( albumUID , AlbumUID ) {
return fmt . Errorf ( "album: invalid uid %s" , clean . Log ( albumUID ) )
} else if err = Db ( ) . Model ( Album { } ) . Where ( "album_uid = ?" , albumUID ) . UpdateColumns ( values ) . Error ; err != nil {
return err
}
return nil
}
2020-06-01 09:45:24 +02:00
// AddPhotoToAlbums adds a photo UID to multiple albums and automatically creates them if needed.
2022-09-30 19:15:10 +02:00
func AddPhotoToAlbums ( uid string , albums [ ] string ) ( err error ) {
return AddPhotoToUserAlbums ( uid , albums , OwnerUnknown )
}
// AddPhotoToUserAlbums adds a photo UID to multiple albums and automatically creates them as a user if needed.
2022-10-02 11:38:30 +02:00
func AddPhotoToUserAlbums ( photoUid string , albums [ ] string , userUid string ) ( err error ) {
if photoUid == "" || len ( albums ) == 0 {
2020-06-01 09:45:24 +02:00
// Do nothing.
2020-05-01 12:57:26 +02:00
return nil
2019-12-06 10:26:57 +01:00
}
2022-10-02 11:38:30 +02:00
if ! rnd . IsUID ( photoUid , PhotoUID ) {
return fmt . Errorf ( "album: can not add invalid photo uid %s" , clean . Log ( photoUid ) )
2020-06-01 09:45:24 +02:00
}
for _ , album := range albums {
2022-10-02 11:38:30 +02:00
var albumUid string
2020-06-01 09:45:24 +02:00
if album == "" {
2022-10-02 11:38:30 +02:00
log . Debugf ( "album: cannot add photo uid %s because album id was not specified" , clean . Log ( photoUid ) )
2020-06-01 09:45:24 +02:00
continue
}
2022-09-28 09:01:17 +02:00
if rnd . IsUID ( album , AlbumUID ) {
2022-10-02 11:38:30 +02:00
albumUid = album
2020-06-01 09:45:24 +02:00
} else {
2023-04-01 14:25:05 +02:00
a := NewUserAlbum ( album , AlbumManual , userUid )
2020-06-01 09:45:24 +02:00
2022-09-28 09:01:17 +02:00
if found := a . Find ( ) ; found != nil {
2022-10-02 11:38:30 +02:00
albumUid = found . AlbumUID
2020-06-01 09:45:24 +02:00
} else if err = a . Create ( ) ; err == nil {
2022-10-02 11:38:30 +02:00
albumUid = a . AlbumUID
2020-06-01 09:45:24 +02:00
} else {
2022-10-02 11:38:30 +02:00
log . Errorf ( "album: %s (add photo %s to albums)" , err . Error ( ) , photoUid )
2020-06-01 09:45:24 +02:00
}
}
2022-10-02 11:38:30 +02:00
if albumUid != "" {
entry := PhotoAlbum { AlbumUID : albumUid , PhotoUID : photoUid , Hidden : false }
2020-06-01 09:45:24 +02:00
if err = entry . Save ( ) ; err != nil {
2022-10-02 11:38:30 +02:00
log . Errorf ( "album: %s (add photo %s to albums)" , err . Error ( ) , photoUid )
2020-06-01 09:45:24 +02:00
}
2023-07-23 14:26:14 +02:00
// Refresh updated timestamp.
err = UpdateAlbum ( albumUid , Values { "updated_at" : TimePointer ( ) } )
2020-06-01 09:45:24 +02:00
}
}
return err
2019-06-04 18:26:35 +02:00
}
2019-06-18 06:37:10 +02:00
2022-09-30 19:15:10 +02:00
// NewAlbum creates a new album of the given type.
2020-05-26 09:02:19 +02:00
func NewAlbum ( albumTitle , albumType string ) * Album {
2022-09-30 19:15:10 +02:00
return NewUserAlbum ( albumTitle , albumType , OwnerUnknown )
}
// NewUserAlbum creates a new album owned by a user.
2022-10-02 11:38:30 +02:00
func NewUserAlbum ( albumTitle , albumType , userUid string ) * Album {
2021-08-29 13:26:05 +02:00
now := TimeStamp ( )
2019-06-18 06:37:10 +02:00
2022-09-30 19:15:10 +02:00
// Set default type.
2020-05-29 12:21:17 +02:00
if albumType == "" {
2023-04-01 14:25:05 +02:00
albumType = AlbumManual
2020-05-29 12:21:17 +02:00
}
2022-09-30 19:15:10 +02:00
// Set default values.
2019-06-18 06:37:10 +02:00
result := & Album {
2023-01-30 12:27:34 +01:00
AlbumOrder : sortby . Oldest ,
2020-05-23 20:58:58 +02:00
AlbumType : albumType ,
2020-04-26 14:31:33 +02:00
CreatedAt : now ,
UpdatedAt : now ,
2022-10-02 11:38:30 +02:00
CreatedBy : userUid ,
2019-06-18 06:37:10 +02:00
}
2022-09-30 19:15:10 +02:00
// Set album title.
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 {
2022-03-24 21:46:25 +01:00
albumSlug := txt . Slug ( albumPath )
2020-12-14 20:37:54 +01:00
if albumTitle == "" || albumSlug == "" || albumPath == "" || albumFilter == "" {
2020-05-30 15:42:04 +02:00
return nil
}
2021-08-29 13:26:05 +02:00
now := TimeStamp ( )
2020-05-30 15:42:04 +02:00
result := & Album {
2023-01-30 12:27:34 +01:00
AlbumOrder : sortby . Added ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumFolder ,
2022-03-24 21:46:25 +01:00
AlbumSlug : txt . Clip ( albumSlug , txt . ClipSlug ) ,
AlbumPath : txt . Clip ( albumPath , txt . ClipPath ) ,
2020-05-30 15:42:04 +02:00
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
2022-03-25 18:01:34 +01:00
result . SetTitle ( albumTitle )
2020-05-30 15:42:04 +02:00
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
}
2021-08-29 13:26:05 +02:00
now := TimeStamp ( )
2020-05-30 01:41:47 +02:00
result := & Album {
2023-01-30 12:27:34 +01:00
AlbumOrder : sortby . Oldest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumMoment ,
2022-03-24 21:46:25 +01:00
AlbumSlug : txt . Clip ( albumSlug , txt . ClipSlug ) ,
2020-06-08 18:32:51 +02:00
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
2022-03-25 18:01:34 +01:00
result . SetTitle ( albumTitle )
2020-06-08 18:32:51 +02:00
return result
}
// NewStateAlbum creates a new moment.
func NewStateAlbum ( albumTitle , albumSlug , albumFilter string ) * Album {
2021-11-23 20:25:32 +01:00
albumTitle = strings . TrimSpace ( albumTitle )
albumSlug = strings . TrimSpace ( albumSlug )
2020-06-08 18:32:51 +02:00
if albumTitle == "" || albumSlug == "" || albumFilter == "" {
return nil
}
2021-08-29 13:26:05 +02:00
now := TimeStamp ( )
2020-06-08 18:32:51 +02:00
result := & Album {
2023-01-30 12:27:34 +01:00
AlbumOrder : sortby . Newest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumState ,
2022-03-24 21:46:25 +01:00
AlbumSlug : txt . Clip ( albumSlug , txt . ClipSlug ) ,
2020-05-30 01:41:47 +02:00
AlbumFilter : albumFilter ,
CreatedAt : now ,
UpdatedAt : now ,
}
2022-03-25 18:01:34 +01:00
result . SetTitle ( albumTitle )
2020-05-30 01:41:47 +02:00
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 {
2021-11-23 20:25:32 +01:00
albumTitle = strings . TrimSpace ( albumTitle )
albumSlug = strings . TrimSpace ( albumSlug )
2023-04-01 14:25:05 +02:00
if albumTitle == "" || albumSlug == "" || year < 1 || month < 1 || month > 12 {
2020-05-30 01:41:47 +02:00
return nil
}
2021-11-26 13:59:10 +01:00
f := form . SearchPhotos {
2021-09-20 23:32:35 +02:00
Year : strconv . Itoa ( year ) ,
Month : strconv . Itoa ( month ) ,
2020-06-05 10:59:59 +02:00
Public : true ,
2020-05-30 01:41:47 +02:00
}
2021-08-29 13:26:05 +02:00
now := TimeStamp ( )
2020-05-30 01:41:47 +02:00
result := & Album {
2023-01-30 12:27:34 +01:00
AlbumOrder : sortby . Oldest ,
2020-06-08 18:32:51 +02:00
AlbumType : AlbumMonth ,
2020-05-30 01:41:47 +02:00
AlbumSlug : albumSlug ,
AlbumFilter : f . Serialize ( ) ,
AlbumYear : year ,
AlbumMonth : month ,
CreatedAt : now ,
UpdatedAt : now ,
}
2022-03-25 18:01:34 +01:00
result . SetTitle ( albumTitle )
2020-05-30 01:41:47 +02:00
return result
}
2021-11-23 20:25:32 +01:00
// FindMonthAlbum finds a matching month album or returns nil.
func FindMonthAlbum ( year , month int ) * Album {
2022-09-28 09:01:17 +02:00
m := Album { }
2021-11-23 20:25:32 +01:00
2023-04-01 14:25:05 +02:00
if year < 1 || month < 1 || month > 12 {
return nil
}
2023-04-02 10:27:57 +02:00
if UnscopedDb ( ) . First ( & m , "album_year = ? AND album_month = ? AND album_type = ?" , year , month , AlbumMonth ) . Error != nil {
2021-11-23 20:25:32 +01:00
return nil
}
2022-09-28 09:01:17 +02:00
return & m
2021-11-23 20:25:32 +01:00
}
2020-06-01 09:45:24 +02:00
// FindAlbumBySlug finds a matching album or returns nil.
2022-09-28 09:01:17 +02:00
func FindAlbumBySlug ( albumSlug , albumType string ) * Album {
m := Album { }
2020-06-01 09:45:24 +02:00
2023-04-01 14:25:05 +02:00
if albumSlug == "" {
return nil
}
2023-04-02 10:27:57 +02:00
if UnscopedDb ( ) . First ( & m , "album_slug = ? AND album_type = ?" , albumSlug , albumType ) . Error != nil {
2022-09-28 09:01:17 +02:00
return nil
}
2020-12-08 22:40:13 +01:00
2022-09-28 09:01:17 +02:00
return & m
2020-12-08 22:40:13 +01:00
}
2021-11-24 13:30:43 +01:00
// FindAlbumByAttr finds an album by filters and slugs, or returns nil.
func FindAlbumByAttr ( slugs , filters [ ] string , albumType string ) * Album {
2022-09-28 09:01:17 +02:00
m := Album { }
2021-11-23 20:25:32 +01:00
2023-04-01 14:25:05 +02:00
if len ( slugs ) == 0 && len ( filters ) == 0 {
return nil
}
2021-11-24 13:30:43 +01:00
stmt := UnscopedDb ( )
if albumType != "" {
stmt = stmt . Where ( "album_type = ?" , albumType )
}
2023-04-01 14:25:05 +02:00
if len ( filters ) == 0 {
stmt = stmt . Where ( "album_slug IN (?)" , slugs )
} else {
stmt = stmt . Where ( "album_slug IN (?) OR album_filter IN (?)" , slugs , filters )
}
2021-11-24 13:30:43 +01:00
2023-04-02 10:27:57 +02:00
if stmt . First ( & m ) . Error != nil {
2021-11-23 20:25:32 +01:00
return nil
}
2022-09-28 09:01:17 +02:00
return & m
2021-11-23 20:25:32 +01:00
}
2020-12-08 22:40:13 +01:00
// FindFolderAlbum finds a matching folder album or returns nil.
2020-12-14 20:37:54 +01:00
func FindFolderAlbum ( albumPath string ) * Album {
albumPath = strings . Trim ( albumPath , string ( os . PathSeparator ) )
2022-03-24 21:46:25 +01:00
albumSlug := txt . Slug ( albumPath )
2020-12-14 20:37:54 +01:00
if albumSlug == "" {
return nil
}
2022-09-28 09:01:17 +02:00
m := Album { }
2020-12-08 22:40:13 +01:00
2022-09-28 09:01:17 +02:00
stmt := UnscopedDb ( ) . Where ( "album_type = ?" , AlbumFolder ) .
Where ( "album_slug = ? OR album_path = ?" , albumSlug , albumPath )
2021-11-24 13:30:43 +01:00
2023-04-02 10:27:57 +02:00
if stmt . First ( & m ) . Error != nil {
2020-06-01 09:45:24 +02:00
return nil
}
2022-09-28 09:01:17 +02:00
return & m
2020-06-01 09:45:24 +02:00
}
2022-09-28 09:01:17 +02:00
// FindAlbum retrieves the matching record from the database and updates the entity.
func FindAlbum ( find Album ) * Album {
m := Album { }
// Search by UID.
if rnd . IsUID ( find . AlbumUID , AlbumUID ) {
2023-04-02 10:27:57 +02:00
if UnscopedDb ( ) . First ( & m , "album_uid = ?" , find . AlbumUID ) . Error != nil {
2022-08-01 15:57:19 +02:00
return nil
2022-09-28 09:01:17 +02:00
} else if m . AlbumUID != "" {
albumCache . SetDefault ( m . AlbumUID , m )
return & m
2020-06-01 09:45:24 +02:00
}
}
2022-09-28 09:01:17 +02:00
// Otherwise, album type and slug are required.
if find . AlbumType == "" || find . AlbumSlug == "" {
return nil
2021-11-24 13:30:43 +01:00
}
2022-09-28 09:01:17 +02:00
// Create search condition.
stmt := UnscopedDb ( ) . Where ( "album_type = ?" , find . AlbumType )
2022-10-02 22:09:02 +02:00
// Search by slug and filter or title.
2023-04-01 14:25:05 +02:00
if find . AlbumType != AlbumManual {
if find . AlbumFilter != "" {
stmt = stmt . Where ( "album_slug = ? OR album_filter = ?" , find . AlbumSlug , find . AlbumFilter )
} else {
stmt = stmt . Where ( "album_slug = ?" , find . AlbumSlug )
}
2021-11-24 13:30:43 +01:00
} else {
2022-09-28 09:01:17 +02:00
stmt = stmt . Where ( "album_slug = ? OR album_title LIKE ?" , find . AlbumSlug , find . AlbumTitle )
2021-11-24 13:30:43 +01:00
}
2022-10-02 22:09:02 +02:00
// Filter by creator if the album has not been published yet.
if find . CreatedBy != "" {
stmt = stmt . Where ( "published_at > ? OR created_by = ?" , TimeStamp ( ) , find . CreatedBy )
}
2022-09-28 09:01:17 +02:00
// Find first matching record.
2023-04-02 10:27:57 +02:00
if stmt . First ( & m ) . Error != nil {
2022-09-28 09:01:17 +02:00
return nil
2022-08-01 15:57:19 +02:00
}
2020-06-01 09:45:24 +02:00
2022-09-28 09:01:17 +02:00
// Cache result.
2022-08-01 15:57:19 +02:00
if m . AlbumUID != "" {
2022-09-28 09:01:17 +02:00
albumCache . SetDefault ( m . AlbumUID , m )
2022-08-01 15:57:19 +02:00
}
2022-09-28 09:01:17 +02:00
return & m
}
2023-04-01 14:25:05 +02:00
// HasID tests if the album has a valid id and uid.
func ( m * Album ) HasID ( ) bool {
return m . ID > 0 && rnd . IsUID ( m . AlbumUID , AlbumUID )
}
2022-09-28 09:01:17 +02:00
// Find retrieves the matching record from the database and updates the entity.
func ( m * Album ) Find ( ) * Album {
return FindAlbum ( * m )
2020-06-01 09:45:24 +02:00
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func ( m * Album ) BeforeCreate ( scope * gorm . Scope ) error {
2022-10-02 11:38:30 +02:00
if rnd . IsUID ( m . AlbumUID , AlbumUID ) {
2020-06-01 09:45:24 +02:00
return nil
}
2022-10-02 11:38:30 +02:00
m . AlbumUID = rnd . GenerateUID ( AlbumUID )
return scope . SetColumn ( "AlbumUID" , m . AlbumUID )
2020-06-01 09:45:24 +02:00
}
// String returns the id or name as string.
func ( m * Album ) String ( ) string {
if m . AlbumSlug != "" {
2022-04-15 09:42:07 +02:00
return clean . Log ( m . AlbumSlug )
2020-06-01 09:45:24 +02:00
}
if m . AlbumTitle != "" {
2022-04-15 09:42:07 +02:00
return clean . Log ( m . AlbumTitle )
2020-06-01 09:45:24 +02:00
}
if m . AlbumUID != "" {
2022-04-15 09:42:07 +02:00
return clean . Log ( m . AlbumUID )
2020-06-01 09:45:24 +02:00
}
return "[unknown album]"
}
2021-09-23 23:46:17 +02:00
// IsMoment tests if the album is of type moment.
2020-05-30 01:41:47 +02:00
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
}
2021-11-23 19:23:10 +01:00
// IsState tests if the album is of type state.
func ( m * Album ) IsState ( ) bool {
return m . AlbumType == AlbumState
}
// IsDefault tests if the album is a regular album.
func ( m * Album ) IsDefault ( ) bool {
2023-04-01 14:25:05 +02:00
return m . AlbumType == AlbumManual
2021-11-23 19:23:10 +01:00
}
2020-05-26 09:02:19 +02:00
// SetTitle changes the album name.
2023-03-13 22:17:23 +01:00
func ( m * Album ) SetTitle ( title string ) * Album {
2022-03-25 18:01:34 +01:00
title = strings . Trim ( title , "_&|{}<>: \n\r\t\\" )
2022-11-28 09:24:23 +01:00
title = strings . ReplaceAll ( title , "\"" , "“" )
2022-03-25 18:01:34 +01:00
title = txt . Shorten ( title , txt . ClipDefault , txt . Ellipsis )
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
}
2022-03-25 18:01:34 +01:00
m . AlbumTitle = title
2020-04-26 14:31:33 +02:00
2023-04-01 14:25:05 +02:00
if m . AlbumType == AlbumManual || m . AlbumSlug == "" {
2020-05-30 21:11:56 +02:00
if len ( m . AlbumTitle ) < txt . ClipSlug {
2021-09-23 23:46:17 +02:00
m . AlbumSlug = txt . Slug ( m . AlbumTitle )
2020-05-30 21:11:56 +02:00
} else {
2021-09-23 23:46:17 +02:00
m . AlbumSlug = txt . Slug ( m . AlbumTitle ) + "-" + m . AlbumUID
2020-05-30 21:11:56 +02:00
}
2020-04-26 14:31:33 +02:00
}
2020-12-27 13:11:08 +01:00
if m . AlbumSlug == "" {
m . AlbumSlug = "-"
}
2023-03-13 22:17:23 +01:00
return m
}
// SetLocation sets a new album location.
func ( m * Album ) SetLocation ( location , state , country string ) * Album {
if location != "" {
m . AlbumLocation = txt . Shorten ( location , txt . ClipDefault , txt . Ellipsis )
}
if state != "" || country != "" && country != "zz" {
m . AlbumCountry = txt . Clip ( country , txt . ClipCountry )
m . AlbumState = txt . Clip ( clean . State ( state , country ) , txt . ClipCategory )
}
return m
2019-12-03 23:55:24 +01:00
}
2020-04-20 10:38:01 +02:00
2023-03-13 22:17:23 +01:00
// UpdateTitleAndLocation updates title, location, and slug of generated albums if needed.
func ( m * Album ) UpdateTitleAndLocation ( title , location , state , country , slug string ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2022-03-24 21:46:25 +01:00
title = txt . Clip ( title , txt . ClipDefault )
slug = txt . Clip ( slug , txt . ClipSlug )
2021-11-23 20:25:32 +01:00
if title == "" || slug == "" {
return nil
}
changed := false
if m . AlbumSlug != slug {
m . AlbumSlug = slug
changed = true
}
2023-03-13 22:17:23 +01:00
if ! changed && state == m . AlbumState && country == m . AlbumCountry {
2021-11-23 20:25:32 +01:00
return nil
}
2022-03-25 18:01:34 +01:00
if title != "" {
m . SetTitle ( title )
}
2021-11-23 20:25:32 +01:00
2023-03-13 22:17:23 +01:00
// Skip location?
if location == "" && state == "" && ( country == "" || country == "zz" ) {
return m . Updates ( Values {
"album_title" : m . AlbumTitle ,
"album_slug" : m . AlbumSlug ,
} )
}
m . SetLocation ( location , state , country )
return m . Updates ( Values {
"album_title" : m . AlbumTitle ,
"album_location" : m . AlbumLocation ,
"album_state" : m . AlbumState ,
"album_country" : m . AlbumCountry ,
"album_slug" : m . AlbumSlug ,
} )
2021-11-23 20:25:32 +01:00
}
2023-03-13 22:17:23 +01:00
// UpdateTitleAndState updates the album location.
func ( m * Album ) UpdateTitleAndState ( title , slug , stateName , countryCode string ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2022-03-24 21:46:25 +01:00
title = txt . Clip ( title , txt . ClipDefault )
slug = txt . Clip ( slug , txt . ClipSlug )
2021-11-23 20:25:32 +01:00
if title == "" || slug == "" || stateName == "" || countryCode == "" {
2021-11-20 16:36:34 +01:00
return nil
}
changed := false
countryName := maps . CountryName ( countryCode )
if m . AlbumCountry != countryCode {
m . AlbumCountry = countryCode
changed = true
}
if changed || m . AlbumLocation == "" {
m . AlbumLocation = countryName
changed = true
}
if m . AlbumState != stateName {
m . AlbumState = stateName
changed = true
}
2021-11-23 20:25:32 +01:00
if m . AlbumSlug != slug {
m . AlbumSlug = slug
changed = true
}
2021-11-20 16:36:34 +01:00
if ! changed {
return nil
}
2022-03-25 18:01:34 +01:00
if title != "" {
m . SetTitle ( title )
}
2021-11-20 16:36:34 +01:00
2021-11-23 20:25:32 +01:00
return m . Updates ( Values { "album_title" : m . AlbumTitle , "album_slug" : m . AlbumSlug , "album_location" : m . AlbumLocation , "album_country" : m . AlbumCountry , "album_state" : m . AlbumState } )
2021-11-20 16:36:34 +01:00
}
2021-06-02 17:25:04 +02:00
// SaveForm updates 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 != "" {
2021-09-23 23:46:17 +02:00
m . AlbumCategory = txt . Clip ( txt . Title ( f . AlbumCategory ) , txt . ClipCategory )
2020-05-30 21:11:56 +02:00
}
2020-05-26 09:02:19 +02:00
if f . AlbumTitle != "" {
m . SetTitle ( f . AlbumTitle )
2020-04-20 10:38:01 +02:00
}
2022-04-10 14:38:51 +02:00
return m . Save ( )
2020-04-20 10:38:01 +02:00
}
2020-05-26 11:00:39 +02:00
2020-12-14 14:11:14 +01:00
// Update sets a new value for a database column.
2020-05-26 11:00:39 +02:00
func ( m * Album ) Update ( attr string , value interface { } ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2022-04-10 14:38:51 +02:00
return UnscopedDb ( ) . Model ( m ) . Update ( attr , value ) . Error
2020-05-26 11:00:39 +02:00
}
2021-11-20 16:36:34 +01:00
// Updates multiple columns in the database.
func ( m * Album ) Updates ( values interface { } ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2022-04-10 14:38:51 +02:00
return UnscopedDb ( ) . Model ( m ) . Updates ( values ) . Error
2021-11-20 16:36:34 +01:00
}
2020-12-14 14:11:14 +01:00
// UpdateFolder updates the path, filter and slug for a folder album.
func ( m * Album ) UpdateFolder ( albumPath , albumFilter string ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2020-12-14 20:37:54 +01:00
albumPath = strings . Trim ( albumPath , string ( os . PathSeparator ) )
2022-03-24 21:46:25 +01:00
albumSlug := txt . Slug ( albumPath )
2020-12-14 20:37:54 +01:00
2023-04-01 14:25:05 +02:00
if albumSlug == "" || albumPath == "" || albumFilter == "" || ! m . HasID ( ) {
return fmt . Errorf ( "folder album must have a path and filter" )
2020-12-14 20:37:54 +01:00
}
2023-04-02 10:27:57 +02:00
if err := m . Updates ( map [ string ] interface { } {
2020-12-14 20:37:54 +01:00
"AlbumPath" : albumPath ,
2020-12-14 14:11:14 +01:00
"AlbumFilter" : albumFilter ,
2020-12-14 20:37:54 +01:00
"AlbumSlug" : albumSlug ,
2023-04-02 10:27:57 +02:00
} ) ; err != nil {
2020-12-08 22:40:13 +01:00
return err
2023-04-01 14:25:05 +02:00
} else if err = UnscopedDb ( ) . Exec ( "UPDATE albums SET album_path = NULL WHERE album_type = ? AND album_path = ? AND id <> ?" , AlbumFolder , albumPath , m . ID ) . Error ; err != nil {
2020-12-08 22:40:13 +01:00
return err
}
return nil
}
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-26 11:00:39 +02:00
func ( m * Album ) Save ( ) error {
2022-10-02 11:38:30 +02:00
if err := Db ( ) . Save ( m ) . Error ; err != nil {
return err
} else {
event . PublishUserEntities ( "albums" , event . EntityUpdated , [ ] * Album { m } , m . CreatedBy )
return nil
}
2020-05-26 11:00:39 +02:00
}
// 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
}
2021-11-23 19:23:10 +01:00
m . PublishCountChange ( 1 )
2022-10-02 11:38:30 +02:00
event . PublishUserEntities ( "albums" , event . EntityCreated , [ ] * Album { m } , m . CreatedBy )
2021-11-23 19:23:10 +01:00
return nil
}
// PublishCountChange publishes an event with the added or removed number of albums.
func ( m * Album ) PublishCountChange ( n int ) {
data := event . Data { "count" : n }
2020-05-30 01:41:47 +02:00
switch m . AlbumType {
2023-04-01 14:25:05 +02:00
case AlbumManual :
2021-11-23 19:23:10 +01:00
event . Publish ( "count.albums" , data )
2020-06-08 18:32:51 +02:00
case AlbumMoment :
2021-11-23 19:23:10 +01:00
event . Publish ( "count.moments" , data )
2020-06-08 18:32:51 +02:00
case AlbumMonth :
2021-11-23 19:23:10 +01:00
event . Publish ( "count.months" , data )
2020-06-08 18:32:51 +02:00
case AlbumFolder :
2021-11-23 19:23:10 +01:00
event . Publish ( "count.folders" , data )
2020-05-30 01:41:47 +02:00
}
}
2021-09-06 11:19:18 +02:00
// Delete marks the entity as deleted in the database.
func ( m * Album ) Delete ( ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2021-09-06 11:19:18 +02:00
if m . Deleted ( ) {
return nil
}
if err := Db ( ) . Delete ( m ) . Error ; err != nil {
return err
}
2021-11-23 19:23:10 +01:00
m . PublishCountChange ( - 1 )
2022-10-02 11:38:30 +02:00
event . EntitiesDeleted ( "albums" , [ ] string { m . AlbumUID } )
2021-09-06 11:19:18 +02:00
2021-11-23 19:23:10 +01:00
return DeleteShareLinks ( m . AlbumUID )
2021-09-06 11:19:18 +02:00
}
2021-11-20 16:36:34 +01:00
// DeletePermanently permanently removes an album from the index.
func ( m * Album ) DeletePermanently ( ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2021-11-20 16:36:34 +01:00
wasDeleted := m . Deleted ( )
if err := UnscopedDb ( ) . Delete ( m ) . Error ; err != nil {
return err
}
if ! wasDeleted {
2021-11-23 19:23:10 +01:00
m . PublishCountChange ( - 1 )
2022-10-02 11:38:30 +02:00
event . EntitiesDeleted ( "albums" , [ ] string { m . AlbumUID } )
2021-11-20 16:36:34 +01:00
}
2021-11-23 19:23:10 +01:00
return DeleteShareLinks ( m . AlbumUID )
2021-11-20 16:36:34 +01:00
}
2021-09-06 11:19:18 +02:00
// Deleted tests if the entity is deleted.
func ( m * Album ) Deleted ( ) bool {
2022-04-21 22:17:26 +02:00
if m . DeletedAt == nil {
return false
}
return ! m . DeletedAt . IsZero ( )
2021-09-06 11:19:18 +02:00
}
// Restore restores the entity in the database.
func ( m * Album ) Restore ( ) error {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return fmt . Errorf ( "album does not exist" )
}
2021-09-06 11:19:18 +02:00
if ! m . Deleted ( ) {
return nil
}
if err := UnscopedDb ( ) . Model ( m ) . Update ( "DeletedAt" , nil ) . Error ; err != nil {
return err
}
m . DeletedAt = nil
2021-11-23 19:23:10 +01:00
m . PublishCountChange ( 1 )
2022-10-02 11:38:30 +02:00
event . PublishUserEntities ( "albums" , event . EntityCreated , [ ] * Album { m } , m . CreatedBy )
2021-09-06 11:19:18 +02:00
return nil
}
// Title returns the album title.
2020-06-01 09:45:24 +02:00
func ( m * Album ) Title ( ) string {
return m . AlbumTitle
}
2020-05-30 01:41:47 +02:00
2021-11-18 00:46:34 +01:00
// ZipName returns the zip download filename.
func ( m * Album ) ZipName ( ) string {
2022-03-24 21:46:25 +01:00
s := txt . Slug ( m . AlbumTitle )
2021-11-18 00:46:34 +01:00
if len ( s ) < 2 {
s = fmt . Sprintf ( "photoprism-album-%s" , m . AlbumUID )
}
return fmt . Sprintf ( "%s.zip" , s )
}
2020-06-01 09:45:24 +02:00
// AddPhotos adds photos to an existing album.
2020-12-17 18:24:55 +01:00
func ( m * Album ) AddPhotos ( UIDs [ ] string ) ( added PhotoAlbums ) {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return added
}
2023-07-23 14:26:14 +02:00
// Add album entries.
2020-06-01 09:45:24 +02:00
for _ , uid := range UIDs {
2023-04-02 10:27:57 +02:00
if ! rnd . IsUID ( uid , PhotoUID ) {
continue
}
2020-06-01 09:45:24 +02:00
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
}
2023-07-23 14:26:14 +02:00
// Refresh updated timestamp.
if err := UpdateAlbum ( m . AlbumUID , Values { "updated_at" : TimePointer ( ) } ) ; err != nil {
log . Errorf ( "album: %s (update %s)" , err . Error ( ) , m )
}
2020-06-01 09:45:24 +02:00
return added
}
// RemovePhotos removes photos from an album.
2020-12-17 18:24:55 +01:00
func ( m * Album ) RemovePhotos ( UIDs [ ] string ) ( removed PhotoAlbums ) {
2023-04-02 10:27:57 +02:00
if ! m . HasID ( ) {
return removed
}
2020-06-01 09:45:24 +02:00
for _ , uid := range UIDs {
2023-04-02 10:27:57 +02:00
if ! rnd . IsUID ( uid , PhotoUID ) {
continue
}
2020-06-01 09:45:24 +02:00
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 )
}
}
2023-07-25 19:15:09 +02:00
2023-07-25 19:10:01 +02:00
// Refresh updated timestamp.
if err := UpdateAlbum ( m . AlbumUID , Values { "updated_at" : TimePointer ( ) } ) ; err != nil {
log . Errorf ( "album: %s (update %s)" , err . Error ( ) , m )
2023-07-25 19:15:09 +02:00
}
2020-06-01 09:45:24 +02:00
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 )
}