internal/library/library.go

147 lines · 3427 bytes

  1// Package library turns the photo folder tree into albums and items.
  2package library
  3
  4import (
  5	"html/template"
  6	"sort"
  7	"strconv"
  8	"sync/atomic"
  9	"time"
 10
 11	"gitbay.org/cmc/gallery/internal/format"
 12)
 13
 14type Library struct {
 15	Albums     []*Album
 16	About      template.HTML
 17	Incomplete bool // set when the scan found zero albums or an album directory failed to read
 18	Timeline   []Month
 19	bySlug     map[string]*Album
 20}
 21
 22// Month groups every item taken in one calendar month, newest first.
 23type Month struct {
 24	Time  time.Time // first instant of the month
 25	Items []*Item
 26}
 27
 28// buildTimeline groups all items by the month of Item.Taken, newest month and item first.
 29func buildTimeline(albums []*Album) []Month {
 30	var items []*Item
 31	for _, a := range albums {
 32		items = append(items, a.Items...)
 33	}
 34	sort.SliceStable(items, func(i, j int) bool {
 35		if !items[i].Taken.Equal(items[j].Taken) {
 36			return items[i].Taken.After(items[j].Taken)
 37		}
 38		return items[i].Album+"/"+items[i].Name < items[j].Album+"/"+items[j].Name
 39	})
 40	var months []Month
 41	for _, it := range items {
 42		start := time.Date(it.Taken.Year(), it.Taken.Month(), 1, 0, 0, 0, 0, time.UTC)
 43		if n := len(months); n == 0 || !months[n-1].Time.Equal(start) {
 44			months = append(months, Month{Time: start})
 45		}
 46		months[len(months)-1].Items = append(months[len(months)-1].Items, it)
 47	}
 48	return months
 49}
 50
 51func (l *Library) Album(slug string) *Album {
 52	return l.bySlug[slug]
 53}
 54
 55type Album struct {
 56	Slug        string
 57	Title       string
 58	Date        string
 59	Description template.HTML
 60	Order       int
 61	HasOrder    bool
 62	Cover       *Item
 63	Items       []*Item
 64	byName      map[string]int
 65}
 66
 67func (a *Album) Item(name string) (int, *Item) {
 68	i, ok := a.byName[name]
 69	if !ok {
 70		return -1, nil
 71	}
 72	return i, a.Items[i]
 73}
 74
 75// Year is the album's #+date year, else the cover's taken year.
 76func (a *Album) Year() string {
 77	if len(a.Date) >= 4 {
 78		return a.Date[:4]
 79	}
 80	if a.Cover != nil && !a.Cover.Taken.IsZero() {
 81		return strconv.Itoa(a.Cover.Taken.Year())
 82	}
 83	return ""
 84}
 85
 86func (a *Album) sortTime() time.Time {
 87	for _, layout := range []string{"2006-01-02", "2006-01", "2006"} {
 88		if t, err := time.Parse(layout, a.Date); err == nil {
 89			return t
 90		}
 91	}
 92	var newest time.Time
 93	for _, it := range a.Items {
 94		if it.Taken.After(newest) {
 95			newest = it.Taken
 96		}
 97	}
 98	return newest
 99}
100
101type Item struct {
102	Album   string
103	Name    string
104	Path    string
105	ModTime time.Time
106	Size    int64
107	Taken   time.Time // EXIF date if set, else ModTime.
108	Meta    format.Meta
109	Format  format.Format
110}
111
112// Store holds the current Library snapshot.
113type Store struct {
114	p atomic.Pointer[Library]
115}
116
117func (s *Store) Load() *Library { return s.p.Load() }
118func (s *Store) Set(l *Library) { s.p.Store(l) }
119
120// sortAlbums puts albums with #+order first (ascending), then the rest newest first.
121func sortAlbums(as []*Album) {
122	sort.SliceStable(as, func(i, j int) bool {
123		a, b := as[i], as[j]
124		if a.HasOrder != b.HasOrder {
125			return a.HasOrder
126		}
127		if a.HasOrder && a.Order != b.Order {
128			return a.Order < b.Order
129		}
130		if !a.HasOrder {
131			if ta, tb := a.sortTime(), b.sortTime(); !ta.Equal(tb) {
132				return ta.After(tb)
133			}
134		}
135		return a.Slug < b.Slug
136	})
137}
138
139func sortItems(items []*Item, by string) {
140	sort.SliceStable(items, func(i, j int) bool {
141		a, b := items[i], items[j]
142		if by == "taken" && !a.Taken.Equal(b.Taken) {
143			return a.Taken.Before(b.Taken)
144		}
145		return a.Name < b.Name
146	})
147}