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}