// Package library turns the photo folder tree into albums and items. package library import ( "html/template" "sort" "strconv" "sync/atomic" "time" "gitbay.org/cmc/gallery/internal/format" ) type Library struct { Albums []*Album About template.HTML Incomplete bool // set when the scan found zero albums or an album directory failed to read Timeline []Month bySlug map[string]*Album } // Month groups every item taken in one calendar month, newest first. type Month struct { Time time.Time // first instant of the month Items []*Item } // buildTimeline groups all items by the month of Item.Taken, newest month and item first. func buildTimeline(albums []*Album) []Month { var items []*Item for _, a := range albums { items = append(items, a.Items...) } sort.SliceStable(items, func(i, j int) bool { if !items[i].Taken.Equal(items[j].Taken) { return items[i].Taken.After(items[j].Taken) } return items[i].Album+"/"+items[i].Name < items[j].Album+"/"+items[j].Name }) var months []Month for _, it := range items { start := time.Date(it.Taken.Year(), it.Taken.Month(), 1, 0, 0, 0, 0, time.UTC) if n := len(months); n == 0 || !months[n-1].Time.Equal(start) { months = append(months, Month{Time: start}) } months[len(months)-1].Items = append(months[len(months)-1].Items, it) } return months } func (l *Library) Album(slug string) *Album { return l.bySlug[slug] } type Album struct { Slug string Title string Date string Description template.HTML Order int HasOrder bool Cover *Item Items []*Item byName map[string]int } func (a *Album) Item(name string) (int, *Item) { i, ok := a.byName[name] if !ok { return -1, nil } return i, a.Items[i] } // Year is the album's #+date year, else the cover's taken year. func (a *Album) Year() string { if len(a.Date) >= 4 { return a.Date[:4] } if a.Cover != nil && !a.Cover.Taken.IsZero() { return strconv.Itoa(a.Cover.Taken.Year()) } return "" } func (a *Album) sortTime() time.Time { for _, layout := range []string{"2006-01-02", "2006-01", "2006"} { if t, err := time.Parse(layout, a.Date); err == nil { return t } } var newest time.Time for _, it := range a.Items { if it.Taken.After(newest) { newest = it.Taken } } return newest } type Item struct { Album string Name string Path string ModTime time.Time Size int64 Taken time.Time // EXIF date if set, else ModTime. Meta format.Meta Format format.Format } // Store holds the current Library snapshot. type Store struct { p atomic.Pointer[Library] } func (s *Store) Load() *Library { return s.p.Load() } func (s *Store) Set(l *Library) { s.p.Store(l) } // sortAlbums puts albums with #+order first (ascending), then the rest newest first. func sortAlbums(as []*Album) { sort.SliceStable(as, func(i, j int) bool { a, b := as[i], as[j] if a.HasOrder != b.HasOrder { return a.HasOrder } if a.HasOrder && a.Order != b.Order { return a.Order < b.Order } if !a.HasOrder { if ta, tb := a.sortTime(), b.sortTime(); !ta.Equal(tb) { return ta.After(tb) } } return a.Slug < b.Slug }) } func sortItems(items []*Item, by string) { sort.SliceStable(items, func(i, j int) bool { a, b := items[i], items[j] if by == "taken" && !a.Taken.Equal(b.Taken) { return a.Taken.Before(b.Taken) } return a.Name < b.Name }) }