Improve Immich-backed photos endpoints with robust mapping/error handling, full albums CRUD, reliable list pagination/sorting/filtering, and shared Nextcloud quota checks before upload.
174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
package photos
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/paginate"
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
photospkg "github.com/ultisuite/ulti-backend/internal/photos"
|
|
)
|
|
|
|
var photosTimeFormats = []string{
|
|
time.RFC3339Nano,
|
|
time.RFC3339,
|
|
"2006-01-02T15:04:05.000Z",
|
|
"2006-01-02",
|
|
}
|
|
|
|
func parsePhotosTime(raw string) (time.Time, bool) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return time.Time{}, false
|
|
}
|
|
for _, layout := range photosTimeFormats {
|
|
if t, err := time.Parse(layout, raw); err == nil {
|
|
return t.UTC(), true
|
|
}
|
|
}
|
|
return time.Time{}, false
|
|
}
|
|
|
|
func parseSortField(sort string) (field string, desc bool) {
|
|
sort = strings.TrimSpace(sort)
|
|
if sort == "" {
|
|
return "created_at", true
|
|
}
|
|
if strings.HasPrefix(sort, "-") {
|
|
return strings.TrimPrefix(sort, "-"), true
|
|
}
|
|
return sort, false
|
|
}
|
|
|
|
func assetMatchesDate(a photospkg.Asset, from, to *time.Time) bool {
|
|
if from == nil && to == nil {
|
|
return true
|
|
}
|
|
created, ok := parsePhotosTime(a.CreatedAt)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if from != nil && created.Before(*from) {
|
|
return false
|
|
}
|
|
if to != nil && created.After(*to) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func albumMatchesDate(a photospkg.Album, from, to *time.Time) bool {
|
|
if from == nil && to == nil {
|
|
return true
|
|
}
|
|
created, ok := parsePhotosTime(a.CreatedAt)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if from != nil && created.Before(*from) {
|
|
return false
|
|
}
|
|
if to != nil && created.After(*to) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func filterAssets(assets []photospkg.Asset, params query.ListParams) []photospkg.Asset {
|
|
q := strings.ToLower(strings.TrimSpace(params.Q))
|
|
out := make([]photospkg.Asset, 0, len(assets))
|
|
for _, a := range assets {
|
|
if q != "" {
|
|
if !strings.Contains(strings.ToLower(a.OriginalName), q) &&
|
|
!strings.Contains(strings.ToLower(a.MimeType), q) {
|
|
continue
|
|
}
|
|
}
|
|
if !assetMatchesDate(a, params.From, params.To) {
|
|
continue
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func filterAlbums(albums []photospkg.Album, params query.ListParams) []photospkg.Album {
|
|
q := strings.ToLower(strings.TrimSpace(params.Q))
|
|
out := make([]photospkg.Album, 0, len(albums))
|
|
for _, a := range albums {
|
|
if q != "" && !strings.Contains(strings.ToLower(a.Name), q) {
|
|
continue
|
|
}
|
|
if !albumMatchesDate(a, params.From, params.To) {
|
|
continue
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sortAssets(assets []photospkg.Asset, sortParam string) {
|
|
field, desc := parseSortField(sortParam)
|
|
sort.SliceStable(assets, func(i, j int) bool {
|
|
if desc {
|
|
return compareAssets(assets[j], assets[i], field)
|
|
}
|
|
return compareAssets(assets[i], assets[j], field)
|
|
})
|
|
}
|
|
|
|
func compareAssets(a, b photospkg.Asset, field string) bool {
|
|
switch field {
|
|
case "name":
|
|
return strings.ToLower(a.OriginalName) < strings.ToLower(b.OriginalName)
|
|
case "size":
|
|
return a.FileSize < b.FileSize
|
|
default: // created_at
|
|
ti, okI := parsePhotosTime(a.CreatedAt)
|
|
tj, okJ := parsePhotosTime(b.CreatedAt)
|
|
if okI && okJ {
|
|
return ti.Before(tj)
|
|
}
|
|
return a.CreatedAt < b.CreatedAt
|
|
}
|
|
}
|
|
|
|
func sortAlbums(albums []photospkg.Album, sortParam string) {
|
|
field, desc := parseSortField(sortParam)
|
|
sort.SliceStable(albums, func(i, j int) bool {
|
|
if desc {
|
|
return compareAlbums(albums[j], albums[i], field)
|
|
}
|
|
return compareAlbums(albums[i], albums[j], field)
|
|
})
|
|
}
|
|
|
|
func compareAlbums(a, b photospkg.Album, field string) bool {
|
|
switch field {
|
|
case "name":
|
|
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
|
|
case "size":
|
|
return a.AssetCount < b.AssetCount
|
|
default: // created_at
|
|
ti, okI := parsePhotosTime(a.CreatedAt)
|
|
tj, okJ := parsePhotosTime(b.CreatedAt)
|
|
if okI && okJ {
|
|
return ti.Before(tj)
|
|
}
|
|
return a.CreatedAt < b.CreatedAt
|
|
}
|
|
}
|
|
|
|
func applyAssetList(assets []photospkg.Asset, params query.ListParams) ([]photospkg.Asset, int64) {
|
|
filtered := filterAssets(assets, params)
|
|
sortAssets(filtered, params.Sort)
|
|
return paginate.Slice(filtered, params.Offset(), params.Limit())
|
|
}
|
|
|
|
func applyAlbumList(albums []photospkg.Album, params query.ListParams) ([]photospkg.Album, int64) {
|
|
filtered := filterAlbums(albums, params)
|
|
sortAlbums(filtered, params.Sort)
|
|
return paginate.Slice(filtered, params.Offset(), params.Limit())
|
|
}
|