2021-09-18 15:32:39 +02:00
package search
import (
"strings"
2022-09-30 19:15:10 +02:00
"time"
2021-09-18 15:32:39 +02:00
2022-09-30 19:15:10 +02:00
"github.com/dustin/go-humanize/english"
2023-01-02 16:17:59 +01:00
"github.com/jinzhu/gorm"
2022-09-30 19:15:10 +02:00
"github.com/photoprism/photoprism/internal/acl"
2021-09-18 15:32:39 +02:00
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
2023-01-02 16:17:59 +01:00
"github.com/photoprism/photoprism/pkg/rnd"
2023-01-30 12:27:34 +01:00
"github.com/photoprism/photoprism/pkg/sortby"
2021-09-18 15:32:39 +02:00
"github.com/photoprism/photoprism/pkg/txt"
)
2022-09-30 19:15:10 +02:00
// Albums finds AlbumResults based on the search form without checking rights or permissions.
2021-11-26 14:28:50 +01:00
func Albums ( f form . SearchAlbums ) ( results AlbumResults , err error ) {
2022-09-30 19:15:10 +02:00
return UserAlbums ( f , nil )
}
// UserAlbums finds AlbumResults based on the search form and user session.
func UserAlbums ( f form . SearchAlbums , sess * entity . Session ) ( results AlbumResults , err error ) {
start := time . Now ( )
if err = f . ParseQueryString ( ) ; err != nil {
log . Debugf ( "albums: %s" , err )
return AlbumResults { } , err
2021-09-18 15:32:39 +02:00
}
// Base query.
s := UnscopedDb ( ) . Table ( "albums" ) .
2023-03-13 22:17:23 +01:00
Select ( "albums.*, cp.photo_count, cl.link_count, CASE WHEN albums.album_year = 0 THEN 0 ELSE 1 END AS has_year, CASE WHEN albums.album_location = '' THEN 1 ELSE 0 END AS no_location" ) .
2021-09-18 15:32:39 +02: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" ) .
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" ) .
Where ( "albums.deleted_at IS NULL" )
2022-09-30 19:15:10 +02:00
// Check session permissions and apply as needed.
if sess != nil {
user := sess . User ( )
aclRole := user . AclRole ( )
// Determine resource to check.
var aclResource acl . Resource
switch f . Type {
case entity . AlbumDefault :
aclResource = acl . ResourceAlbums
case entity . AlbumFolder :
aclResource = acl . ResourceFolders
case entity . AlbumMoment :
aclResource = acl . ResourceMoments
case entity . AlbumMonth :
aclResource = acl . ResourceCalendar
case entity . AlbumState :
aclResource = acl . ResourcePlaces
2021-11-18 12:54:26 +01:00
}
2022-10-02 22:09:02 +02:00
// Check user permissions.
2022-09-30 19:15:10 +02:00
if acl . Resources . DenyAll ( aclResource , aclRole , acl . Permissions { acl . AccessAll , acl . AccessLibrary , acl . AccessShared , acl . AccessOwn } ) {
return AlbumResults { } , ErrForbidden
}
2021-11-18 12:54:26 +01:00
2022-09-30 19:15:10 +02:00
// Limit results by UID, owner and path.
2022-10-02 22:09:02 +02:00
if sess . IsVisitor ( ) || sess . NotRegistered ( ) {
s = s . Where ( "albums.album_uid IN (?) OR albums.published_at > ?" , sess . SharedUIDs ( ) , entity . TimeStamp ( ) )
2022-09-30 19:15:10 +02:00
} else if acl . Resources . DenyAll ( aclResource , aclRole , acl . Permissions { acl . AccessAll , acl . AccessLibrary } ) {
2023-02-20 15:54:33 +01:00
if basePath := user . GetBasePath ( ) ; basePath == "" {
2022-10-02 22:09:02 +02:00
s = s . Where ( "albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ?" , sess . SharedUIDs ( ) , user . UserUID , entity . TimeStamp ( ) )
2022-09-30 19:15:10 +02:00
} else {
2022-10-02 22:09:02 +02:00
s = s . Where ( "albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ? OR albums.album_type = ? AND (albums.album_path = ? OR albums.album_path LIKE ?)" ,
2023-02-20 15:54:33 +01:00
sess . SharedUIDs ( ) , user . UserUID , entity . TimeStamp ( ) , entity . AlbumFolder , basePath , basePath + "/%" )
2021-11-18 12:54:26 +01:00
}
2022-09-30 19:15:10 +02:00
}
2021-11-18 12:54:26 +01:00
2022-09-30 19:15:10 +02:00
// Exclude private content?
if acl . Resources . Deny ( acl . ResourcePhotos , aclRole , acl . AccessPrivate ) || acl . Resources . Deny ( aclResource , aclRole , acl . AccessPrivate ) {
f . Public = true
f . Private = false
2021-11-18 12:54:26 +01:00
}
}
2021-09-18 15:32:39 +02:00
// Set sort order.
switch f . Order {
2023-01-30 12:27:34 +01:00
case sortby . Count :
2021-11-18 00:46:34 +01:00
s = s . Order ( "photo_count DESC, albums.album_title, albums.album_uid DESC" )
2023-03-13 22:17:23 +01:00
case sortby . Moment , sortby . Newest :
2023-01-02 16:49:18 +01:00
if f . Type == entity . AlbumDefault || f . Type == entity . AlbumState {
s = s . Order ( "albums.album_uid DESC" )
2023-03-13 22:17:23 +01:00
} else if f . Type == entity . AlbumMoment {
s = s . Order ( "has_year, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC" )
2023-01-02 16:49:18 +01:00
} else {
s = s . Order ( "albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC" )
}
2023-01-30 12:27:34 +01:00
case sortby . Oldest :
2023-01-02 16:49:18 +01:00
if f . Type == entity . AlbumDefault || f . Type == entity . AlbumState {
s = s . Order ( "albums.album_uid ASC" )
2023-03-13 22:17:23 +01:00
} else if f . Type == entity . AlbumMoment {
s = s . Order ( "has_year, albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC" )
2023-01-02 16:49:18 +01:00
} else {
s = s . Order ( "albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC" )
}
2023-01-30 12:27:34 +01:00
case sortby . Added :
2021-11-18 00:46:34 +01:00
s = s . Order ( "albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Edited :
2023-01-04 14:15:07 +01:00
s = s . Order ( "albums.updated_at DESC, albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Place :
2023-03-13 22:17:23 +01:00
s = s . Order ( "no_location, albums.album_location, has_year, albums.album_year DESC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Path :
2021-11-18 12:54:26 +01:00
s = s . Order ( "albums.album_path, albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Category :
2021-11-18 00:46:34 +01:00
s = s . Order ( "albums.album_category, albums.album_title, albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Slug :
2023-01-02 15:04:50 +01:00
s = s . Order ( "albums.album_slug ASC, albums.album_uid DESC" )
2023-01-30 12:27:34 +01:00
case sortby . Favorites :
2023-01-04 14:15:07 +01:00
if f . Type == entity . AlbumFolder {
s = s . Order ( "albums.album_favorite DESC, albums.album_path ASC, albums.album_uid DESC" )
} else if f . Type == entity . AlbumMonth {
s = s . Order ( "albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC" )
} else {
s = s . Order ( "albums.album_favorite DESC, albums.album_title ASC, albums.album_uid DESC" )
}
2023-01-30 12:27:34 +01:00
case sortby . Name :
2023-01-02 17:59:48 +01:00
if f . Type == entity . AlbumFolder {
s = s . Order ( "albums.album_path ASC, albums.album_uid DESC" )
} else {
s = s . Order ( "albums.album_title ASC, albums.album_uid DESC" )
}
2023-03-13 22:17:23 +01:00
case sortby . NameReverse :
if f . Type == entity . AlbumFolder {
s = s . Order ( "albums.album_path DESC, albums.album_uid DESC" )
} else {
s = s . Order ( "albums.album_title DESC, albums.album_uid DESC" )
}
2021-09-18 15:32:39 +02:00
default :
2021-11-29 14:48:42 +01:00
s = s . Order ( "albums.album_favorite DESC, albums.album_title ASC, albums.album_uid DESC" )
2021-09-18 15:32:39 +02:00
}
2022-09-30 19:15:10 +02:00
// Find specific UIDs only?
if txt . NotEmpty ( f . UID ) {
ids := SplitOr ( strings . ToLower ( f . UID ) )
if rnd . ContainsUID ( ids , entity . AlbumUID ) {
s = s . Where ( "albums.album_uid IN (?)" , ids )
}
}
// Filter by title or path?
if txt . NotEmpty ( f . Query ) {
if f . Type != entity . AlbumFolder {
likeString := "%" + f . Query + "%"
s = s . Where ( "albums.album_title LIKE ? OR albums.album_location LIKE ?" , likeString , likeString )
} else {
2023-01-02 17:59:48 +01:00
searchQuery := strings . Trim ( strings . ReplaceAll ( f . Query , "\\" , "/" ) , "/" )
for _ , where := range LikeAllNames ( Cols { "albums.album_title" , "albums.album_location" , "albums.album_path" } , searchQuery ) {
s = s . Where ( where )
2022-09-30 19:15:10 +02:00
}
2021-09-18 15:32:39 +02:00
}
2022-09-30 19:15:10 +02:00
}
2021-09-18 15:32:39 +02:00
2022-09-30 19:15:10 +02:00
// Albums with public pictures only?
if f . Public {
2023-02-21 00:02:44 +01:00
s = s . Where ( "albums.album_private = 0 AND (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))" )
2022-09-30 19:15:10 +02:00
} else {
s = s . Where ( "albums.album_type <> 'folder' OR albums.album_path IN (SELECT photo_path FROM photos WHERE photo_quality > -1 AND deleted_at IS NULL)" )
2021-09-18 15:32:39 +02:00
}
2022-09-30 19:15:10 +02:00
if txt . NotEmpty ( f . Type ) {
2021-09-18 15:32:39 +02:00
s = s . Where ( "albums.album_type IN (?)" , strings . Split ( f . Type , txt . Or ) )
}
2022-09-30 19:15:10 +02:00
if txt . NotEmpty ( f . Category ) {
2021-09-18 15:32:39 +02:00
s = s . Where ( "albums.album_category IN (?)" , strings . Split ( f . Category , txt . Or ) )
}
2022-09-30 19:15:10 +02:00
if txt . NotEmpty ( f . Location ) {
2021-09-18 15:32:39 +02:00
s = s . Where ( "albums.album_location IN (?)" , strings . Split ( f . Location , txt . Or ) )
}
2022-09-30 19:15:10 +02:00
if txt . NotEmpty ( f . Country ) {
2021-09-18 15:32:39 +02:00
s = s . Where ( "albums.album_country IN (?)" , strings . Split ( f . Country , txt . Or ) )
}
2023-01-02 16:17:59 +01:00
// Favorites only?
2021-09-18 15:32:39 +02:00
if f . Favorite {
s = s . Where ( "albums.album_favorite = 1" )
}
2023-01-02 16:17:59 +01:00
// Filter by year?
if txt . NotEmpty ( f . Year ) {
// Filter by the pictures included if it is a manually managed album, as these do not have an explicit
// year assigned to them, unlike calendar albums and moments for example.
if f . Type == entity . AlbumDefault {
s = s . Where ( "? OR albums.album_uid IN (SELECT DISTINCT pay.album_uid FROM photos_albums pay " +
"JOIN photos py ON pay.photo_uid = py.photo_uid WHERE py.photo_year IN (?) AND pay.hidden = 0 AND pay.missing = 0)" ,
gorm . Expr ( AnyInt ( "albums.album_year" , f . Year , txt . Or , entity . UnknownYear , txt . YearMax ) ) , strings . Split ( f . Year , txt . Or ) )
} else {
s = s . Where ( AnyInt ( "albums.album_year" , f . Year , txt . Or , entity . UnknownYear , txt . YearMax ) )
}
2021-09-18 15:32:39 +02:00
}
2023-01-02 16:17:59 +01:00
// Filter by month?
if txt . NotEmpty ( f . Month ) {
s = s . Where ( AnyInt ( "albums.album_month" , f . Month , txt . Or , entity . UnknownMonth , txt . MonthMax ) )
2021-09-18 15:32:39 +02:00
}
2023-01-02 16:17:59 +01:00
// Filter by day?
if txt . NotEmpty ( f . Day ) {
s = s . Where ( AnyInt ( "albums.album_day" , f . Day , txt . Or , entity . UnknownDay , txt . DayMax ) )
2021-09-18 15:32:39 +02:00
}
2022-09-30 19:15:10 +02: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 )
}
// Query database.
2021-09-18 15:32:39 +02:00
if result := s . Scan ( & results ) ; result . Error != nil {
return results , result . Error
}
2022-09-30 19:15:10 +02:00
// Log number of results.
log . Debugf ( "albums: found %s [%s]" , english . Plural ( len ( results ) , "result" , "results" ) , time . Since ( start ) )
2021-09-18 15:32:39 +02:00
return results , nil
}