ref: e96cb917d4fa016ce4575f4e8eb5a05fedd5a1a7
parent: 14c50ed0b3050906015c48c987460865bfd7f30c
author: Alex Musolino <alex@musolino.id.au>
date: Fri Dec 8 19:13:51 EST 2023
imgsrv: add tag album views
--- a/album.tpl
+++ b/album.tpl
@@ -42,7 +42,7 @@
{{else}}
<span class="disabled">prev</span>
{{end}}
- | <a id="up" href="../index.html">index</a> |
+ | <a id="up" href="{{.UpLink}}">{{.UpText}}</a> |
{{if .Next}}
<a id="next" href="{{.Next}}">next</a>
{{else}}
@@ -49,7 +49,7 @@
<span class="disabled">next</span>
{{end}}
</p>
-{{range .Images}}<a href="{{.}}.html"><img src="{{.}}.thumb.JPG"/></a>
+{{range .Images}}<a href="{{.ID}}.html"><img src="{{.Prefix}}{{.ID}}.thumb.JPG"/></a>
{{end}}
<p>
{{if .Prev}}
--- a/image.tpl
+++ b/image.tpl
@@ -38,15 +38,19 @@
</script>
</head>
<body>
-<p>{{if .Prev}}<a id="prev" href="{{.Prev}}">prev</a>{{else}}<span class="disabled">prev</span>{{end}} | <a id="up" href=".">up</a> | {{if .Next}}<a id="next" href="{{.Next}}">next</a>{{else}}<span class="disabled">next</span>{{end}}</p>
-<p><a href="{{.Image}}.full.JPG"><img src="{{.Image}}.big.JPG"/></a></p>
-{{range .ImgTags}} <a href="#">#{{.}}</a>{{else}}<br />{{end}}
+<p>{{if .Prev}}<a id="prev" href="{{.Prev}}">prev</a>{{else}}<span class="disabled">prev</span>{{end}} | <a id="up" href=".">{{.UpText}}</a> | {{if .Next}}<a id="next" href="{{.Next}}">next</a>{{else}}<span class="disabled">next</span>{{end}}</p>
+<p><a href="{{.Prefix}}{{.Image}}.full.JPG"><img src="{{.Prefix}}{{.Image}}.big.JPG"/></a></p>
+{{range .ImgTags}} <a href="/tags/{{.}}">#{{.}}</a>{{else}}<br />{{end}}
<p>
+<div style="display: inline-block; width: 15cm;">
<form action="/api/tag" method="post">
<input type="hidden" name="image" value="{{.Image}}" />
{{range .Tags}}<input type="submit" name="tags" value="#{{.}}" />
{{end}}
</form>
+</div>
+</p>
+<p>
<form action="/api/tag" method="post">
<input type="hidden" name="image" value="{{.Image}}" />
<input id="tag-list" type="text" name="tags" />
--- a/imgsrv.go
+++ b/imgsrv.go
@@ -63,21 +63,37 @@
}
func (h *AlbumIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
+ relpath := strings.TrimPrefix(r.URL.Path, "/")
+ log.Printf("AlbumIndexHandler: relpath=%s\n", relpath)
+ switch relpath {
case "":
fallthrough
case "index.html":
+ type TplImgData struct {
+ ID string
+ Prefix string
+ }
type TplData struct {
Title string
+ UpLink, UpText string
Prev, Next string
- Images []string
+ Images []TplImgData
}
tplData := TplData{
Title: path.Base(h.Idx.Path),
- Images: h.Idx.Images,
+ UpLink: "/index.html",
+ UpText: "index",
}
+ for _, img := range h.Idx.Images {
+ tplData.Images = append(tplData.Images, TplImgData{
+ ID: img,
+ Prefix: fmt.Sprintf("/%s/%s/", img[0:4], img[4:6]),
+ })
+ }
if h.Idx.Year != 0 {
tplData.Title = fmt.Sprintf("%s %d", time.Month(h.Idx.Month).String()[0:3], h.Idx.Year)
+ tplData.UpLink = fmt.Sprintf("/%d/index.html", h.Idx.Year)
+ tplData.UpText = fmt.Sprintf("%d", h.Idx.Year)
}
tplData.Title = fmt.Sprintf("Photos :: %s", tplData.Title)
if h.Idx.Year != 0 {
@@ -94,26 +110,30 @@
}
return
}
- if strings.HasSuffix(r.URL.Path, ".html") {
+ if strings.HasSuffix(relpath, ".html") {
type TplData struct {
Title string
+ UpText string
Prev, Next string
+ Prefix string
Image string
ImgTags []string
Tags []string
}
- image, _ := strings.CutSuffix(r.URL.Path, ".html")
+ image, _ := strings.CutSuffix(relpath, ".html")
tplData := TplData{
Title: path.Base(h.Idx.Path),
+ UpText: "up",
Next: h.Idx.Next(image, ".html"),
Prev: h.Idx.Prev(image, ".html"),
+ Prefix: fmt.Sprintf("/%s/%s/", image[0:4], image[4:6]),
Image: image,
ImgTags: h.Tags.TagsForImage(image),
Tags: h.Tags.ShortList(),
}
- log.Printf("%s has tags: %v\n", image, tplData.Tags)
if h.Idx.Year != 0 {
tplData.Title = fmt.Sprintf("%s %d", time.Month(h.Idx.Month).String()[0:3], h.Idx.Year)
+ tplData.UpText = fmt.Sprintf("%d/%02d", h.Idx.Year, h.Idx.Month+1)
}
tplData.Title = fmt.Sprintf("Photos :: %s :: %s", tplData.Title, image)
if err := h.ImageTpl.Execute(w, tplData); err != nil {
@@ -121,7 +141,7 @@
}
return
}
- if strings.HasSuffix(strings.ToLower(r.URL.Path), ".jpg") {
+ if strings.HasSuffix(strings.ToLower(relpath), ".jpg") {
http.ServeFile(w, r, fmt.Sprintf("%s/%s", h.Idx.Path, r.URL.Path))
return
}
@@ -131,6 +151,7 @@
type MainIndexHandler struct {
DB *ImgDB
Tpl *template.Template
+ Tags *Tags
}
func (h *MainIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -138,11 +159,13 @@
Title string
Years sort.StringSlice
Albums sort.StringSlice
+ Tags []string
}
tplData := TplData{
Title: "Photos",
Years: make([]string, 0, len(h.DB.Years)),
Albums: make([]string, 0, len(h.DB.Albums)),
+ Tags: h.Tags.Tags(),
}
for year := range h.DB.Years {
tplData.Years = append(tplData.Years, year)
@@ -420,29 +443,31 @@
ImgLUT StrLUT
NewBorns StrLUT
DeathRow StrLUT
+ LRU []string
+ MaxLRU int
}
-func NewTags() *Tags {
+func NewTags(maxlru int) *Tags {
return &Tags{
TagLUT: make(StrLUT),
ImgLUT: make(StrLUT),
NewBorns: make(StrLUT),
DeathRow: make(StrLUT),
+ MaxLRU: maxlru,
}
}
-func loadTags(tagDir, img string) (*Tags, error) {
+func loadTags(tags *Tags, tagDir, img string) error {
entries, err := os.ReadDir(fmt.Sprintf("%s/%s", tagDir, img))
if err != nil {
- return nil, err
+ return err
}
- tags := NewTags()
for _, e := range entries {
if e.Type().IsRegular() {
- tags.Tag(img, e.Name())
+ tags.insert(img, e.Name())
}
}
- return tags, nil
+ return nil
}
func OpenTags(path string) (*Tags, error) {
@@ -450,13 +475,11 @@
if err != nil {
return nil, err
}
- tags := NewTags()
+ tags := NewTags(10)
for _, e := range entries {
if e.IsDir() {
- if t, err := loadTags(path, e.Name()); err != nil {
+ if err := loadTags(tags, path, e.Name()); err != nil {
log.Printf("could not load tags for %s: %v\n", e.Name(), err)
- } else {
- tags.Acc(t)
}
}
}
@@ -467,6 +490,17 @@
t.RLock()
defer t.RUnlock()
var tags []string
+ for _, tag := range t.LRU {
+ tags = append(tags, tag)
+ }
+ sort.Strings(tags)
+ return tags
+}
+
+func (t *Tags) Tags() []string {
+ t.RLock()
+ defer t.RUnlock()
+ var tags []string
for tag := range t.TagLUT {
tags = append(tags, tag)
}
@@ -483,9 +517,7 @@
t.ImgLUT.Acc(u.ImgLUT)
}
-func (t *Tags) Tag(img, tag string) {
- t.Lock()
- defer t.Unlock()
+func (t *Tags) insert(img, tag string) {
t.TagLUT.Add(tag, img)
t.ImgLUT.Add(img, tag)
t.NewBorns.Add(img, tag)
@@ -492,6 +524,31 @@
t.DeathRow.Del(img, tag)
}
+func (t *Tags) Tag(img, tag string) {
+ t.Lock()
+ defer t.Unlock()
+ t.insert(img, tag)
+ if t.MaxLRU > 0 {
+ for i := range t.LRU {
+ if t.LRU[i] == tag {
+ for i < len(t.LRU)-1 {
+ t.LRU[i] = t.LRU[i+1]
+ i++
+ }
+ t.LRU[i] = tag
+ break
+ }
+ }
+ nlru := len(t.LRU)
+ if nlru == 0 || t.LRU[nlru-1] != tag {
+ if nlru == t.MaxLRU {
+ t.LRU = t.LRU[1:]
+ }
+ t.LRU = append(t.LRU, tag)
+ }
+ }
+}
+
func (t *Tags) Untag(img, tag string) {
t.Lock()
defer t.Unlock()
@@ -512,7 +569,9 @@
func (t *Tags) ImagesForTag(tag string) []string {
t.RLock()
defer t.RUnlock()
- return t.TagLUT.Lookup(tag)
+ images := t.TagLUT.Lookup(tag)
+ sort.Strings(images)
+ return images
}
func (t *Tags) Flush(path string) error {
@@ -533,7 +592,8 @@
}
for img, tags := range t.DeathRow {
for tag := range tags {
- if err := os.Remove(fmt.Sprintf("%s/%s/%s", path, img, tag)); err != nil {
+ err := os.Remove(fmt.Sprintf("%s/%s/%s", path, img, tag))
+ if err != nil && !os.IsNotExist(err) {
return err
}
}
@@ -582,6 +642,32 @@
http.Redirect(w, r, fmt.Sprintf("/%s/%s/%s.html", img[0:4], img[4:6], img), http.StatusSeeOther)
}
+type TagIndexHandler struct {
+ DB *ImgDB
+ IndexTpl *template.Template
+ ImageTpl *template.Template
+ Tags *Tags
+}
+
+func (h *TagIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ parts := strings.Split(r.URL.Path, "/")
+ log.Printf("TagIndexHandler::ServeHTTP: tag=%s\n", parts[0])
+ if len(parts) == 1 && !strings.HasSuffix(r.URL.Path, "/") {
+ http.Redirect(w, r, fmt.Sprintf("/tags/%s/", parts[0]), http.StatusSeeOther)
+ return
+ }
+ http.StripPrefix(parts[0] + "/", &AlbumIndexHandler{
+ Idx: &AlbumIdx{
+ DB: h.DB,
+ Path: fmt.Sprintf("/tags/%s/", parts[0]),
+ Images: h.Tags.ImagesForTag(parts[0]),
+ },
+ IndexTpl: h.IndexTpl,
+ ImageTpl: h.ImageTpl,
+ Tags: h.Tags,
+ }).ServeHTTP(w, r)
+}
+
func loadTemplates(path string) (*Templates, error) {
mainTpl, err := template.ParseFiles(fmt.Sprintf("%s/main.tpl", path))
if err != nil {
@@ -642,10 +728,11 @@
prefix := fmt.Sprintf("/%s/", y)
http.Handle(prefix, http.StripPrefix(prefix, &YearIndexHandler{yIdx, templates.Year}))
}
+ http.Handle("/tags/", http.StripPrefix("/tags/", &TagIndexHandler{db, templates.Album, templates.Image, tags}))
for album, idx := range db.Albums {
prefix := fmt.Sprintf("/%s/", album)
http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{idx, templates.Album, templates.Image, tags}))
}
- http.Handle("/", &MainIndexHandler{db, templates.Main})
+ http.Handle("/", &MainIndexHandler{db, templates.Main, tags})
log.Fatal(http.ListenAndServe(":8080", nil))
}
--- a/main.tpl
+++ b/main.tpl
@@ -28,5 +28,9 @@
{{range .Years}}
<div><a href="{{.}}/index.html"><img src="{{.}}/montage.jpg"/><p>{{.}}</p></a></div>
{{end}}
+<hr style="clear: both;" />
+{{range .Tags}}
+<a href="/tags/{{.}}">#{{.}}</a>
+{{end}}
</body>
</html>