2020-01-05 14:18:40 +01:00
package query
2019-12-11 07:37:39 +01:00
import (
"fmt"
"strings"
"time"
2019-12-11 16:55:18 +01:00
"github.com/photoprism/photoprism/internal/entity"
2019-12-11 07:37:39 +01:00
"github.com/photoprism/photoprism/internal/form"
2020-01-13 11:07:09 +01:00
"github.com/photoprism/photoprism/pkg/capture"
2020-07-06 07:41:33 +02:00
"github.com/photoprism/photoprism/pkg/txt"
2019-12-11 07:37:39 +01:00
)
// AlbumResult contains found albums
type AlbumResult struct {
2020-05-26 09:02:19 +02:00
ID uint ` json:"-" `
AlbumUID string ` json:"UID" `
2021-08-30 18:58:27 +02:00
ParentUID string ` json:"ParentUID" `
Thumb string ` json:"Thumb" `
ThumbSrc string ` json:"ThumbSrc" `
2020-05-26 09:02:19 +02:00
AlbumSlug string ` json:"Slug" `
AlbumType string ` json:"Type" `
AlbumTitle string ` json:"Title" `
2020-07-12 16:36:39 +02:00
AlbumLocation string ` json:"Location" `
2020-05-26 09:02:19 +02:00
AlbumCategory string ` json:"Category" `
AlbumCaption string ` json:"Caption" `
AlbumDescription string ` json:"Description" `
AlbumNotes string ` json:"Notes" `
AlbumFilter string ` json:"Filter" `
AlbumOrder string ` json:"Order" `
AlbumTemplate string ` json:"Template" `
2020-12-08 19:47:27 +01:00
AlbumPath string ` json:"Path" `
2020-05-26 09:02:19 +02:00
AlbumCountry string ` json:"Country" `
AlbumYear int ` json:"Year" `
AlbumMonth int ` json:"Month" `
2020-07-06 07:41:33 +02:00
AlbumDay int ` json:"Day" `
2020-05-26 09:02:19 +02:00
AlbumFavorite bool ` json:"Favorite" `
AlbumPrivate bool ` json:"Private" `
PhotoCount int ` json:"PhotoCount" `
LinkCount int ` json:"LinkCount" `
CreatedAt time . Time ` json:"CreatedAt" `
UpdatedAt time . Time ` json:"UpdatedAt" `
DeletedAt time . Time ` json:"DeletedAt,omitempty" `
2019-12-11 07:37:39 +01:00
}
2020-06-23 07:55:33 +02:00
type AlbumResults [ ] AlbumResult
2020-05-23 20:58:58 +02:00
// AlbumByUID returns a Album based on the UID.
func AlbumByUID ( albumUID string ) ( album entity . Album , err error ) {
2020-06-22 15:16:26 +02:00
if err := Db ( ) . Where ( "album_uid = ?" , albumUID ) . First ( & album ) . Error ; err != nil {
2019-12-11 07:37:39 +01:00
return album , err
}
return album , nil
}
2020-05-30 01:41:47 +02:00
// AlbumCoverByUID returns a album preview file based on the uid.
2021-02-07 19:04:17 +01:00
func AlbumCoverByUID ( uid string ) ( file entity . File , err error ) {
2020-05-30 01:41:47 +02:00
a := entity . Album { }
2021-09-06 11:19:18 +02:00
if err := UnscopedDb ( ) . Where ( "album_uid = ?" , uid ) . First ( & a ) . Error ; err != nil {
2020-05-30 01:41:47 +02:00
return file , err
2020-06-08 18:32:51 +02:00
} else if a . AlbumType != entity . AlbumDefault { // TODO: Optimize
2020-05-30 14:52:47 +02:00
f := form . PhotoSearch { Album : a . AlbumUID , Filter : a . AlbumFilter , Order : entity . SortOrderRelevance , Count : 1 , Offset : 0 , Merged : false }
2020-05-30 01:41:47 +02:00
if photos , _ , err := PhotoSearch ( f ) ; err != nil {
return file , err
} else if len ( photos ) > 0 {
for _ , photo := range photos {
2020-05-30 14:52:47 +02:00
if err := Db ( ) . Where ( "photo_uid = ? AND file_primary = 1" , photo . PhotoUID ) . First ( & file ) . Error ; err != nil {
return file , err
} else {
return file , nil
}
2020-05-30 01:41:47 +02:00
}
}
2021-09-06 11:19:18 +02:00
// Automatically hide empty months.
if a . AlbumType == entity . AlbumMonth {
if err := a . Delete ( ) ; err != nil {
log . Errorf ( "album: %s (hide %s)" , err , a . AlbumType )
} else {
log . Infof ( "album: %s hidden" , txt . Quote ( a . AlbumTitle ) )
}
}
return file , fmt . Errorf ( "no cover found" )
2020-05-30 01:41:47 +02:00
}
2020-05-30 14:52:47 +02:00
if err := Db ( ) . Where ( "files.file_primary = 1 AND files.file_missing = 0 AND files.file_type = 'jpg' AND files.deleted_at IS NULL" ) .
2021-02-07 19:04:17 +01:00
Joins ( "JOIN albums ON albums.album_uid = ?" , uid ) .
2020-11-21 18:05:20 +01:00
Joins ( "JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = files.photo_uid AND pa.hidden = 0" ) .
2020-04-27 17:06:36 +02:00
Joins ( "JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL" ) .
2020-04-27 14:25:04 +02:00
Order ( "photos.photo_quality DESC, photos.taken_at DESC" ) .
2019-12-11 07:37:39 +01:00
First ( & file ) . Error ; err != nil {
return file , err
}
return file , nil
}
2020-05-30 21:31:32 +02:00
// AlbumPhotos returns up to count photos from an album.
func AlbumPhotos ( a entity . Album , count int ) ( results PhotoResults , err error ) {
results , _ , err = PhotoSearch ( form . PhotoSearch {
Album : a . AlbumUID ,
Filter : a . AlbumFilter ,
Count : count ,
Offset : 0 ,
} )
return results , err
}
2020-05-25 19:10:44 +02:00
// AlbumSearch searches albums based on their name.
2020-06-23 07:55:33 +02:00
func AlbumSearch ( f form . AlbumSearch ) ( results AlbumResults , err error ) {
2019-12-11 07:37:39 +01:00
if err := f . ParseQueryString ( ) ; err != nil {
return results , err
}
2020-05-23 20:58:58 +02:00
defer log . Debug ( capture . Time ( time . Now ( ) , fmt . Sprintf ( "albums: search %s" , form . Serialize ( f , true ) ) ) )
2019-12-11 07:37:39 +01:00
2021-01-20 12:08:48 +01:00
// Base query.
2020-06-23 07:55:33 +02:00
s := UnscopedDb ( ) . Table ( "albums" ) .
Select ( "albums.*, cp.photo_count, cl.link_count" ) .
2020-12-27 13:11:08 +01:00
Joins ( "LEFT JOIN (SELECT album_uid, count(photo_uid) AS photo_count FROM photos_albums WHERE hidden = 0 AND missing = 0 GROUP BY album_uid) AS cp ON cp.album_uid = albums.album_uid" ) .
2020-06-23 07:55:33 +02:00
Joins ( "LEFT JOIN (SELECT share_uid, count(share_uid) AS link_count FROM links GROUP BY share_uid) AS cl ON cl.share_uid = albums.album_uid" ) .
2021-02-08 14:52:33 +01:00
Where ( "albums.album_type <> 'folder' OR albums.album_path IN (SELECT photo_path FROM photos WHERE photo_private = 0 AND photo_quality > -1 AND deleted_at IS NULL)" ) .
2020-06-23 07:55:33 +02:00
Where ( "albums.deleted_at IS NULL" )
2019-12-11 07:37:39 +01:00
2021-01-20 12:08:48 +01:00
// Limit result count.
if f . Count > 0 && f . Count <= MaxResults {
s = s . Limit ( f . Count ) . Offset ( f . Offset )
} else {
s = s . Limit ( MaxResults ) . Offset ( f . Offset )
}
// Set sort order.
switch f . Order {
case "slug" :
s = s . Order ( "albums.album_favorite DESC, album_slug ASC" )
default :
s = s . Order ( "albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.created_at DESC" )
}
2020-01-30 18:19:26 +01:00
if f . ID != "" {
2020-12-31 12:34:06 +01:00
s = s . Where ( "albums.album_uid IN (?)" , strings . Split ( f . ID , Or ) )
2020-01-30 18:19:26 +01:00
2020-03-28 17:17:41 +01:00
if result := s . Scan ( & results ) ; result . Error != nil {
2020-01-30 18:19:26 +01:00
return results , result . Error
}
return results , nil
}
2019-12-11 07:37:39 +01:00
if f . Query != "" {
2020-12-15 20:14:06 +01:00
likeString := "%" + f . Query + "%"
s = s . Where ( "albums.album_title LIKE ? OR albums.album_location LIKE ?" , likeString , likeString )
2019-12-11 07:37:39 +01:00
}
2020-05-29 12:21:17 +02:00
if f . Type != "" {
2020-12-31 12:34:06 +01:00
s = s . Where ( "albums.album_type IN (?)" , strings . Split ( f . Type , Or ) )
2020-05-29 12:21:17 +02:00
}
if f . Category != "" {
2020-12-31 12:34:06 +01:00
s = s . Where ( "albums.album_category IN (?)" , strings . Split ( f . Category , Or ) )
2020-05-29 12:21:17 +02:00
}
2020-07-12 16:36:39 +02:00
if f . Location != "" {
2020-12-31 12:34:06 +01:00
s = s . Where ( "albums.album_location IN (?)" , strings . Split ( f . Location , Or ) )
2020-07-12 16:36:39 +02:00
}
2021-01-16 12:48:43 +01:00
if f . Country != "" {
s = s . Where ( "albums.album_country IN (?)" , strings . Split ( f . Country , Or ) )
}
2020-05-14 19:03:12 +02:00
if f . Favorite {
2020-03-28 17:17:41 +01:00
s = s . Where ( "albums.album_favorite = 1" )
2019-12-11 07:37:39 +01:00
}
2021-08-16 00:29:36 +02:00
if ( f . Year > 0 && f . Year <= txt . YearMax ) || f . Year == entity . UnknownYear {
2020-07-06 07:41:33 +02:00
s = s . Where ( "albums.album_year = ?" , f . Year )
}
2021-08-16 00:29:36 +02:00
if ( f . Month >= txt . MonthMin && f . Month <= txt . MonthMax ) || f . Month == entity . UnknownMonth {
2020-07-06 07:41:33 +02:00
s = s . Where ( "albums.album_month = ?" , f . Month )
}
2021-08-16 00:29:36 +02:00
if ( f . Day >= txt . DayMin && f . Month <= txt . DayMax ) || f . Day == entity . UnknownDay {
2020-07-06 07:41:33 +02:00
s = s . Where ( "albums.album_day = ?" , f . Day )
}
2020-03-28 17:17:41 +01:00
if result := s . Scan ( & results ) ; result . Error != nil {
2019-12-11 07:37:39 +01:00
return results , result . Error
}
return results , nil
}
2020-12-09 13:10:21 +01:00
// UpdateAlbumDates updates album year, month and day based on indexed photo metadata.
func UpdateAlbumDates ( ) error {
2020-12-09 21:44:39 +01:00
switch DbDialect ( ) {
case MySQL :
return UnscopedDb ( ) . Exec ( ` UPDATE albums
INNER JOIN
( SELECT photo_path , MAX ( taken_at_local ) AS taken_max
FROM photos WHERE taken_src = ' meta ' AND photos . photo_quality >= 3 AND photos . deleted_at IS NULL
GROUP BY photo_path ) AS p ON albums . album_path = p . photo_path
SET albums . album_year = YEAR ( taken_max ) , albums . album_month = MONTH ( taken_max ) , albums . album_day = DAY ( taken_max )
WHERE albums . album_type = ' folder ' AND albums . album_path IS NOT NULL AND p . taken_max IS NOT NULL ` ) . Error
default :
return nil
}
2020-12-09 13:10:21 +01:00
}
2020-12-17 18:24:55 +01:00
2020-12-27 13:11:08 +01:00
// UpdateMissingAlbumEntries sets a flag for missing photo album entries.
func UpdateMissingAlbumEntries ( ) error {
switch DbDialect ( ) {
default :
2021-01-16 12:48:43 +01:00
return UnscopedDb ( ) . Exec ( ` UPDATE photos_albums SET missing = 1 WHERE photo_uid IN
2020-12-27 13:11:08 +01:00
( SELECT photo_uid FROM photos WHERE deleted_at IS NOT NULL OR photo_quality < 0 ) ` ) . Error
}
}
// AlbumEntryFound removes the missing flag from album entries.
func AlbumEntryFound ( uid string ) error {
switch DbDialect ( ) {
default :
return UnscopedDb ( ) . Exec ( ` UPDATE photos_albums SET missing = 0 WHERE photo_uid = ? ` , uid ) . Error
}
}
2020-12-17 18:24:55 +01:00
// GetAlbums returns a slice of albums.
func GetAlbums ( offset , limit int ) ( results entity . Albums , err error ) {
err = UnscopedDb ( ) . Table ( "albums" ) . Select ( "*" ) . Offset ( offset ) . Limit ( limit ) . Find ( & results ) . Error
return results , err
}