taggart

Simple golang tagging filesystem webapp
Log | Files | Refs

commit e816ef129b28396eee408834eed55634c5c29ec9
parent bccb8d2e28f775e2f30bdd6638b9edb3d9cbc590
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Tue, 16 Dec 2025 16:31:13 +0000

Add /tag/name/previews function

Can be used with /and/ function too

Diffstat:
Mmain.go | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mtemplates/_header.html | 3++-
Mtemplates/tags.html | 1+
3 files changed, 168 insertions(+), 16 deletions(-)

diff --git a/main.go b/main.go @@ -99,6 +99,13 @@ type VideoFile struct { EscapedFilename string } +type filter struct { + Category string + Value string + Values []string // Expanded values including aliases + IsPreviews bool // New field to indicate preview mode +} + func expandTagWithAliases(category, value string) []string { values := []string{value} @@ -1013,15 +1020,9 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { fullPath := strings.TrimPrefix(r.URL.Path, "/tag/") tagPairs := strings.Split(fullPath, "/and/tag/") - type filter struct { - Category string - Value string - Values []string // Expanded values including aliases - } - breadcrumbs := []Breadcrumb{ {Name: "Home", URL: "/"}, - {Name: "Tags", URL: "/tags"}, // or wherever your tags overview page is + {Name: "Tags", URL: "/tags"}, } var filters []filter @@ -1035,12 +1036,13 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { } f := filter{ - Category: parts[0], - Value: parts[1], + Category: parts[0], + Value: parts[1], + IsPreviews: parts[1] == "previews", } - // Expand with aliases - if parts[1] != "unassigned" { + // Expand with aliases (unless it's a special tag) + if parts[1] != "unassigned" && parts[1] != "previews" { f.Values = expandTagWithAliases(parts[0], parts[1]) } @@ -1075,7 +1077,41 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { }) } - // Build count query + // Check if we're in preview mode for any filter + hasPreviewFilter := false + for _, f := range filters { + if f.IsPreviews { + hasPreviewFilter = true + break + } + } + + if hasPreviewFilter { + // Handle preview mode + files, err := getPreviewFiles(filters) + if err != nil { + renderError(w, "Failed to fetch preview files", http.StatusInternalServerError) + return + } + + var titleParts []string + for _, f := range filters { + titleParts = append(titleParts, fmt.Sprintf("%s: %s", f.Category, f.Value)) + } + title := "Tagged: " + strings.Join(titleParts, " + ") + + pageData := buildPageDataWithPagination(title, ListData{ + Tagged: files, + Untagged: nil, + Breadcrumbs: []Breadcrumb{}, + }, 1, len(files), len(files)) + pageData.Breadcrumbs = breadcrumbs + + renderTemplate(w, "list.html", pageData) + return + } + + // Build count query (existing logic) countQuery := `SELECT COUNT(DISTINCT f.id) FROM files f WHERE 1=1` countArgs := []interface{}{} @@ -1120,7 +1156,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { return } - // Build main query with pagination + // Build main query with pagination (existing logic) query := `SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description FROM files f WHERE 1=1` args := []interface{}{} @@ -1177,13 +1213,127 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { pageData := buildPageDataWithPagination(title, ListData{ Tagged: files, Untagged: nil, - Breadcrumbs: []Breadcrumb{}, // Empty here + Breadcrumbs: []Breadcrumb{}, }, page, total, perPage) - pageData.Breadcrumbs = breadcrumbs // Set at PageData level + pageData.Breadcrumbs = breadcrumbs renderTemplate(w, "list.html", pageData) } +// getPreviewFiles returns one representative file for each tag value in the specified category +func getPreviewFiles(filters []filter) ([]File, error) { + // Find the preview filter category + var previewCategory string + for _, f := range filters { + if f.IsPreviews { + previewCategory = f.Category + break + } + } + + if previewCategory == "" { + return []File{}, nil + } + + // First, get all tag values for the preview category that have files + tagQuery := ` + SELECT DISTINCT t.value + FROM tags t + JOIN categories c ON t.category_id = c.id + JOIN file_tags ft ON ft.tag_id = t.id + WHERE c.name = ? + ORDER BY t.value` + + tagRows, err := db.Query(tagQuery, previewCategory) + if err != nil { + return nil, fmt.Errorf("failed to query tag values: %w", err) + } + defer tagRows.Close() + + var tagValues []string + for tagRows.Next() { + var tagValue string + if err := tagRows.Scan(&tagValue); err != nil { + return nil, fmt.Errorf("failed to scan tag value: %w", err) + } + tagValues = append(tagValues, tagValue) + } + + + if len(tagValues) == 0 { + return []File{}, nil + } + + // For each tag value, find one representative file + var allFiles []File + for _, tagValue := range tagValues { + // Build query for this specific tag value with all filters applied + query := `SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description + FROM files f + WHERE 1=1` + args := []interface{}{} + + // Apply all filters (including the preview category with this specific value) + for _, filter := range filters { + if filter.IsPreviews { + // For the preview filter, use the current tag value we're iterating over + query += ` + AND EXISTS ( + SELECT 1 + FROM file_tags ft + JOIN tags t ON ft.tag_id = t.id + JOIN categories c ON c.id = t.category_id + WHERE ft.file_id = f.id AND c.name = ? AND t.value = ? + )` + args = append(args, filter.Category, tagValue) + } else if filter.Value == "unassigned" { + query += ` + AND NOT EXISTS ( + SELECT 1 + FROM file_tags ft + JOIN tags t ON ft.tag_id = t.id + JOIN categories c ON c.id = t.category_id + WHERE ft.file_id = f.id AND c.name = ? + )` + args = append(args, filter.Category) + } else { + // Normal filter with aliases + placeholders := make([]string, len(filter.Values)) + for i := range filter.Values { + placeholders[i] = "?" + } + + query += fmt.Sprintf(` + AND EXISTS ( + SELECT 1 + FROM file_tags ft + JOIN tags t ON ft.tag_id = t.id + JOIN categories c ON c.id = t.category_id + WHERE ft.file_id = f.id AND c.name = ? AND t.value IN (%s) + )`, strings.Join(placeholders, ",")) + + args = append(args, filter.Category) + for _, v := range filter.Values { + args = append(args, v) + } + } + } + + query += ` ORDER BY f.id DESC LIMIT 1` + + files, err := queryFilesWithTags(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query files for tag %s: %w", tagValue, err) + } + + if len(files) > 0 { + allFiles = append(allFiles, files[0]) + } + } + + return allFiles, nil +} + func loadConfig() error { config = Config{ DatabasePath: "./database.db", diff --git a/templates/_header.html b/templates/_header.html @@ -23,7 +23,8 @@ <a href="/tags#tag-{{$cat}}">{{$cat}}</a> <ul> {{range $tags}}<li><a href="/tag/{{$cat}}/{{.Value}}">{{.Value}} ({{.Count}})</a></li> - {{end}}<li><a href="/tag/{{$cat}}/unassigned">Unassigned</a></li> + {{end}}<li><a href="/tag/{{$cat}}/previews">Previews</a></li> + <li><a href="/tag/{{$cat}}/unassigned">Unassigned</a></li> </ul> </li>{{end}} <li><a href="/bulk-tag">Bulk Editor</a></li> diff --git a/templates/tags.html b/templates/tags.html @@ -9,6 +9,7 @@ {{range $tags}} <li><a href="/tag/{{$cat}}/{{.Value}}">{{.Value}} ({{.Count}})</a></li> {{end}} + <li><a href="/tag/{{$cat}}/previews">Previews</a></li> <li><a href="/tag/{{$cat}}/unassigned">Unassigned</a></li> </ul> </li>