package search import ( "strings" "time" "github.com/dustin/go-humanize/english" "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) // Albums finds AlbumResults based on the search form without checking rights or permissions. func Albums(f form.SearchAlbums) (results AlbumResults, err error) { 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 } // Base query. s := UnscopedDb().Table("albums"). Select("albums.*, cp.photo_count, cl.link_count, CASE WHEN albums.album_year = 0 THEN 0 ELSE 1 END AS has_year"). 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") // 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 } // Check user permissions. if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary, acl.AccessShared, acl.AccessOwn}) { return AlbumResults{}, ErrForbidden } // Limit results by UID, owner and path. if sess.IsVisitor() || sess.NotRegistered() { s = s.Where("albums.album_uid IN (?) OR albums.published_at > ?", sess.SharedUIDs(), entity.TimeStamp()) } else if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) { if user.BasePath == "" { s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp()) } else { 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 ?)", sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), entity.AlbumFolder, user.BasePath, user.BasePath+"/%") } } // 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 } } // Set sort order. switch f.Order { case entity.SortOrderCount: s = s.Order("photo_count DESC, albums.album_title, albums.album_uid DESC") case entity.SortOrderRelevance: s = s.Order("albums.album_favorite DESC, albums.updated_at DESC, albums.album_uid DESC") case entity.SortOrderNewest: if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState { s = s.Order("albums.album_uid DESC") } else { s = s.Order("albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC") } case entity.SortOrderOldest: if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState { s = s.Order("albums.album_uid ASC") } else { s = s.Order("albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC") } case entity.SortOrderAdded: s = s.Order("albums.album_uid DESC") case entity.SortOrderMoment: s = s.Order("albums.album_favorite DESC, has_year, albums.album_year DESC, albums.album_month DESC, albums.album_title ASC, albums.album_uid DESC") case entity.SortOrderPlace: s = s.Order("albums.album_location, albums.album_title, albums.album_year DESC, albums.album_month ASC, albums.album_day ASC, albums.album_uid DESC") case entity.SortOrderPath: s = s.Order("albums.album_path, albums.album_uid DESC") case entity.SortOrderCategory: s = s.Order("albums.album_category, albums.album_title, albums.album_uid DESC") case entity.SortOrderSlug: s = s.Order("albums.album_slug ASC, albums.album_uid DESC") case entity.SortOrderName: 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") } default: s = s.Order("albums.album_favorite DESC, albums.album_title ASC, albums.album_uid DESC") } // 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 { 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) } } } // Albums with public pictures only? if f.Public { s = s.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)") } 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)") } if txt.NotEmpty(f.Type) { s = s.Where("albums.album_type IN (?)", strings.Split(f.Type, txt.Or)) } if txt.NotEmpty(f.Category) { s = s.Where("albums.album_category IN (?)", strings.Split(f.Category, txt.Or)) } if txt.NotEmpty(f.Location) { s = s.Where("albums.album_location IN (?)", strings.Split(f.Location, txt.Or)) } if txt.NotEmpty(f.Country) { s = s.Where("albums.album_country IN (?)", strings.Split(f.Country, txt.Or)) } // Favorites only? if f.Favorite { s = s.Where("albums.album_favorite = 1") } // 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)) } } // Filter by month? if txt.NotEmpty(f.Month) { s = s.Where(AnyInt("albums.album_month", f.Month, txt.Or, entity.UnknownMonth, txt.MonthMax)) } // Filter by day? if txt.NotEmpty(f.Day) { s = s.Where(AnyInt("albums.album_day", f.Day, txt.Or, entity.UnknownDay, txt.DayMax)) } // 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. if result := s.Scan(&results); result.Error != nil { return results, result.Error } // Log number of results. log.Debugf("albums: found %s [%s]", english.Plural(len(results), "result", "results"), time.Since(start)) return results, nil }