shithub: img

Download patch

ref: 58170f38922d0c4c8b7eb78abb02536ec5a560ed
parent: 08f0b154171793f440928929fbca6e49ac085738
author: Alex Musolino <alex@musolino.id.au>
date: Sun Nov 26 14:59:39 EST 2023

imgsrv.go: fix prev/next bugs, drop debug prints, initial work on tagging

--- a/imgsrv.go
+++ b/imgsrv.go
@@ -1,8 +1,10 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"html/template"
+	"io"
 	"log"
 	"net/http"
 	"os"
@@ -9,6 +11,7 @@
 	"path"
 	"sort"
 	"strings"
+	"sync"
 	"time"
 )
 
@@ -18,7 +21,6 @@
 }
 
 func (h *YearIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	log.Printf("YearIndexHandler.ServeHTTP: %s\n", r.URL.Path)
 	if r.URL.Path == "montage.jpg" {
 		http.ServeFile(w, r, fmt.Sprintf("%s/montage.jpg", h.Idx.Path))
 		return
@@ -31,13 +33,15 @@
 	}
 
 	type TplData struct {
-		PrevYear string
-		CurrYear string
-		NextYear string
+		Title string
+		Prev, Next string
+		Curr string
 		Months [12]MonthTplData
 	}
 	tplData := TplData{
-		CurrYear: path.Base(h.Idx.Path),
+		Title: fmt.Sprintf("Photos :: %s", path.Base(h.Idx.Path)),
+		Next: h.Idx.Next(),
+		Prev: h.Idx.Prev(),
 	}
 	for i := 0; i < 12; i++ {
 		if h.Idx.Months[i] != nil {
@@ -57,10 +61,10 @@
 	Idx *AlbumIdx
 	IndexTpl *template.Template
 	ImageTpl *template.Template
+	Tags *Tags
 }
 
 func (h *AlbumIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	log.Printf("AlbumIndexHandler.ServeHTTP: %s\n", r.URL.Path)
 	switch r.URL.Path {
 	case "":
 		fallthrough
@@ -75,12 +79,16 @@
 			Images: h.Idx.Images,
 		}
 		if h.Idx.Year != 0 {
+			tplData.Title = fmt.Sprintf("%s %d", time.Month(h.Idx.Month).String()[0:3], h.Idx.Year)
+		}
+		tplData.Title = fmt.Sprintf("Photos :: %s", tplData.Title)
+		if h.Idx.Year != 0 {
 			yearStr := fmt.Sprintf("%d", h.Idx.Year)
-			if next := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, +1); next != "" {
-				tplData.Next = "../../" + next
+			if next := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, +1); next != nil {
+				tplData.Next = fmt.Sprintf("../../%d/%02d", next.Year, next.Month + 1)
 			}
-			if prev := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, -1); prev != "" {
-				tplData.Prev = "../../" + prev
+			if prev := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, -1); prev != nil {
+				tplData.Prev = fmt.Sprintf("../../%d/%02d", prev.Year, prev.Month + 1)
 			}
 		}
 		if err := h.IndexTpl.Execute(w, tplData); err != nil {
@@ -93,6 +101,7 @@
 			Title string
 			Prev, Next string
 			Image string
+			Tags []string
 		}
 		image, _ := strings.CutSuffix(r.URL.Path, ".html")
 		tplData := TplData{
@@ -100,7 +109,12 @@
 			Next: h.Idx.Next(image, ".html"),
 			Prev: h.Idx.Prev(image, ".html"),
 			Image: image,
+			Tags: h.Tags.TagsForImage(image),
 		}
+		if h.Idx.Year != 0 {
+			tplData.Title = fmt.Sprintf("%s %d", time.Month(h.Idx.Month).String()[0:3], h.Idx.Year)
+		}
+		tplData.Title = fmt.Sprintf("Photos :: %s :: %s", tplData.Title, image)
 		if err := h.ImageTpl.Execute(w, tplData); err != nil {
 			log.Printf("error executing template: %v\n", err)
 		}
@@ -119,12 +133,13 @@
 }
 
 func (h *MainIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	log.Printf("IndexHandler.ServeHTTP\n")
 	type TplData struct {
+		Title string
 		Years sort.StringSlice
 		Albums sort.StringSlice
 	}
 	tplData := TplData{
+		Title: "Photos",
 		Years: make([]string, 0, len(h.DB.Years)),
 		Albums: make([]string, 0, len(h.DB.Albums)),
 	}
@@ -166,6 +181,15 @@
 			return a.Images[i] + suffix
 		}
 	}
+	if a.Year != 0 {
+		if mIdx := a.DB.nextMonth(fmt.Sprintf("%d", a.Year), a.Month, step); mIdx != nil {
+			i = 0
+			if step < 0 {
+				i = len(mIdx.Images)-1
+			}
+			return fmt.Sprintf("../../%d/%02d/%s.html", mIdx.Year, mIdx.Month + 1, mIdx.Images[i])
+		}
+	}
 	return ""
 }
 
@@ -183,6 +207,41 @@
 	Months [12]*AlbumIdx
 }
 
+func (yIdx *YearIdx) next(step int) *YearIdx {
+	var years []string
+	for y := range yIdx.DB.Years {
+		years = append(years, y)
+	}
+	sort.Strings(years)
+	i := 0
+	y0 := path.Base(yIdx.Path)
+	for i < len(years) {
+		if years[i] == y0 {
+			break
+		}
+		i++
+	}
+	i += step
+	if 0 <= i && i < len(years) {
+		return yIdx.DB.Years[years[i]]
+	}
+	return nil
+}
+
+func (yIdx *YearIdx) Next() string {
+	if next := yIdx.next(+1); next != nil {
+		return path.Base(next.Path)
+	}
+	return ""
+}
+
+func (yIdx *YearIdx) Prev() string {
+	if prev := yIdx.next(-1); prev != nil {
+		return path.Base(prev.Path)
+	}
+	return ""
+}
+
 type ImgDB struct {
 	Path string
 	Years map[string]*YearIdx
@@ -189,7 +248,7 @@
 	Albums map[string]*AlbumIdx
 }
 
-func (db *ImgDB) nextMonth(y0 string, m0, step int) string {
+func (db *ImgDB) nextMonth(y0 string, m0, step int) *AlbumIdx {
 	var years []string
 	for y := range db.Years {
 		years = append(years, y)
@@ -203,10 +262,9 @@
 		i0++
 	}
 	for i := i0; 0 <= i && i < len(years); i += step {
-		log.Printf("y=%s, m0=%d, i=%d\n", years[i], m0+1, i)
 		for m := m0 + step; 0 <= m && m < 12; m += step {
-			if db.Years[years[i]].Months[m] != nil {
-				return fmt.Sprintf("%s/%02d", years[i], m + 1)
+			if res := db.Years[years[i]].Months[m]; res != nil {
+				return res
 			}
 		}
 		if step > 0 {
@@ -215,7 +273,7 @@
 			m0 = 12
 		}
 	}
-	return ""
+	return nil
 }
 
 type Templates struct {
@@ -265,7 +323,7 @@
 	return &yearIdx, nil
 }
 
-func loadDatabase(path string, yearRanges []YearRange, albums []string) (*ImgDB, error) {
+func loadImageDatabase(path string, yearRanges []YearRange, albums []string) (*ImgDB, error) {
 	db := &ImgDB{
 		Path: path,
 		Years: make(map[string]*YearIdx),
@@ -298,6 +356,140 @@
 	return db, nil
 }
 
+type StrLUT map[string]map[string]struct{}
+
+func (lut StrLUT) Acc(other StrLUT) {
+	for k1, obin := range other {
+		bin := lut[k1]
+		if bin == nil {
+			bin = make(map[string]struct{})
+			lut[k1] = bin
+		}
+		for k2 := range obin {
+			bin[k2] = struct{}{}
+		}
+	}
+}
+
+func (lut StrLUT) Add(k, v string) {
+	bin := lut[k]
+	if bin == nil {
+		bin = make(map[string]struct{})
+	}
+	bin[v] = struct{}{}
+	lut[k] = bin
+}
+
+func (lut StrLUT) Del(k, v string) {
+	if bin, ok := lut[k]; ok {
+		delete(bin, v)
+	}
+}
+
+func (lut StrLUT) Lookup(s string) []string {
+	var res []string
+	if bin := lut[s]; bin != nil {
+		for k := range bin {
+			res = append(res, k)
+		}
+	}
+	return res
+}
+
+type Tags struct {
+	sync.RWMutex
+	TagLUT StrLUT
+	ImgLUT StrLUT
+}
+
+func NewTags() *Tags {
+	return &Tags{
+		TagLUT: make(StrLUT),
+		ImgLUT: make(StrLUT),
+	}
+}
+
+func (t *Tags) Acc(u *Tags) {
+	u.RLock()
+	defer u.RUnlock()
+	t.Lock()
+	defer t.Unlock()
+	t.TagLUT.Acc(u.TagLUT)
+	t.ImgLUT.Acc(u.ImgLUT)
+}
+
+func (t *Tags) Tag(img, tag string) {
+	t.Lock()
+	defer t.Unlock()
+	t.TagLUT.Add(tag, img)
+	t.ImgLUT.Add(img, tag)
+}
+
+func (t *Tags) Untag(img, tag string) {
+	t.Lock()
+	defer t.Unlock()
+	t.TagLUT.Del(tag, img)
+	t.ImgLUT.Del(img, tag)
+}
+
+func (t *Tags) TagsForImage(img string) []string {
+	t.RLock()
+	defer t.RUnlock()
+	tags := t.ImgLUT.Lookup(img)
+	sort.Strings(tags)
+	return tags
+}
+
+func (t *Tags) ImagesForTag(tag string) []string {
+	t.RLock()
+	defer t.RUnlock()
+	return t.TagLUT.Lookup(tag)	
+}
+
+func parseTagList(r io.Reader) (*Tags, error) {
+	t := NewTags()
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		line := s.Text()
+		if line == "" || line[0] == '#' {
+			continue
+		}
+		f := strings.Fields(line)
+		if len(f) < 2 {
+			return nil, fmt.Errorf("bad format: expected at least 2 fields, got %d", len(f))
+		}
+		for i := 1; i < len(f); i++ {
+			t.Tag(f[0], f[i])
+		}
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+func loadTags(path string) (*Tags, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return parseTagList(bufio.NewReader(f))
+}
+
+type TagApiHandler struct {
+	Tags *Tags
+}
+
+func (h *TagApiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	t, err := parseTagList(r.Body)
+	if err != nil {
+		http.Error(w, "bad request", 400)
+		return
+	}
+	h.Tags.Acc(t)
+}
+
 func loadTemplates(path string) (*Templates, error) {
 	mainTpl, err := template.ParseFiles(fmt.Sprintf("%s/main.tpl", path))
 	if err != nil {
@@ -339,16 +531,20 @@
 	if err != nil {
 		log.Fatalf("could not load templates: %v\n", err)
 	}
-	db, err := loadDatabase(".", yearRanges, albums)
+	db, err := loadImageDatabase(".", yearRanges, albums)
 	if err != nil {
 		log.Fatalf("could not load database: %v\n", err)
 	}
+	tags, err := loadTags("tags")
+	if err != nil {
+		log.Fatalf("could not load tags: %v\n", err)
+	}
+	http.Handle("/api/tag", &TagApiHandler{tags})
 	for y, yIdx := range db.Years {
 		for m, mIdx := range yIdx.Months {
 			if mIdx != nil {
 				prefix := fmt.Sprintf("/%s/%02d/", y, m+1)
-				log.Printf("adding handler for %s\n", prefix)
-				http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{mIdx, templates.Album, templates.Image}))
+				http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{mIdx, templates.Album, templates.Image, tags}))
 			}
 		}
 		prefix := fmt.Sprintf("/%s/", y)
@@ -356,7 +552,7 @@
 	}
 	for album, idx := range db.Albums {
 		prefix := fmt.Sprintf("/%s/", album)
-		http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{idx, templates.Album, templates.Image}))
+		http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{idx, templates.Album, templates.Image, tags}))
 	}
 	http.Handle("/", &MainIndexHandler{db, templates.Main})
 	log.Fatal(http.ListenAndServe(":8080", nil))