shithub: img

Download patch

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>