tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

include-properties.go (5957B)


      1 package main
      2 
      3 import (
      4 	"fmt"
      5 	"image"
      6 	_ "image/gif"
      7 	_ "image/jpeg"
      8 	_ "image/png"
      9 	"log"
     10 	"net/http"
     11 	"os"
     12 	"os/exec"
     13 	"path/filepath"
     14 	"strconv"
     15 	"strings"
     16 )
     17 
     18 func computeProperties(fileID int64, filePath string) {
     19 	ext := strings.ToLower(filepath.Ext(filePath))
     20 
     21 	setProperty(fileID, "filetype", strings.TrimPrefix(ext, "."))
     22 
     23 	switch ext {
     24 	case ".jpg", ".jpeg", ".png", ".gif", ".webp":
     25 		computeImageProperties(fileID, filePath)
     26 	case ".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v":
     27 		computeVideoProperties(fileID, filePath)
     28 	}
     29 }
     30 
     31 func setProperty(fileID int64, key, value string) {
     32 	if value == "" {
     33 		return
     34 	}
     35 	_, err := db.Exec(
     36 		`INSERT OR IGNORE INTO file_properties (file_id, key, value) VALUES (?, ?, ?)`,
     37 		fileID, key, value,
     38 	)
     39 	if err != nil {
     40 		log.Printf("Warning: failed to set property %s for file %d: %v", key, fileID, err)
     41 	}
     42 }
     43 
     44 func computeImageProperties(fileID int64, filePath string) {
     45 	f, err := os.Open(filePath)
     46 	if err != nil {
     47 		log.Printf("Warning: could not open image for properties %s: %v", filePath, err)
     48 		return
     49 	}
     50 	defer f.Close()
     51 
     52 	cfg, _, err := image.DecodeConfig(f)
     53 	if err != nil {
     54 		log.Printf("Warning: could not decode image config %s: %v", filePath, err)
     55 		return
     56 	}
     57 
     58 	w, h := cfg.Width, cfg.Height
     59 
     60 	var orientation string
     61 	switch {
     62 	case w > h:
     63 		orientation = "landscape"
     64 	case h > w:
     65 		orientation = "portrait"
     66 	default:
     67 		orientation = "square"
     68 	}
     69 	setProperty(fileID, "orientation", orientation)
     70 
     71 	mp := w * h
     72 	var tier string
     73 	switch {
     74 	case mp < 1_000_000:
     75 		tier = "small"
     76 	case mp < 4_000_000:
     77 		tier = "medium"
     78 	case mp < 12_000_000:
     79 		tier = "large"
     80 	default:
     81 		tier = "huge"
     82 	}
     83 	setProperty(fileID, "resolution", tier)
     84 }
     85 
     86 func computeVideoProperties(fileID int64, filePath string) {
     87 	cmd := exec.Command("ffprobe",
     88 		"-v", "error",
     89 		"-show_entries", "format=duration",
     90 		"-of", "default=nokey=1:noprint_wrappers=1",
     91 		filePath,
     92 	)
     93 	out, err := cmd.Output()
     94 	if err != nil {
     95 		log.Printf("Warning: ffprobe failed for %s: %v", filePath, err)
     96 		return
     97 	}
     98 
     99 	seconds, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
    100 	if err != nil {
    101 		log.Printf("Warning: could not parse duration for %s: %v", filePath, err)
    102 		return
    103 	}
    104 
    105 	var bucket string
    106 	switch {
    107 	case seconds < 60:
    108 		bucket = "tiny"
    109 	case seconds < 300:
    110 		bucket = "short"
    111 	case seconds < 2700:
    112 		bucket = "moderate"
    113 	default:
    114 		bucket = "long"
    115 	}
    116 	setProperty(fileID, "duration", bucket)
    117 }
    118 
    119 func computeMissingProperties() (int, error) {
    120     rows, err := db.Query(`
    121         SELECT f.id, f.path
    122         FROM files f
    123         WHERE NOT EXISTS (
    124             SELECT 1 FROM file_properties fp WHERE fp.file_id = f.id
    125         )
    126     `)
    127     if err != nil {
    128         return 0, fmt.Errorf("failed to query files: %w", err)
    129     }
    130 
    131     type fileRow struct {
    132         id   int64
    133         path string
    134     }
    135     var files []fileRow
    136     for rows.Next() {
    137         var r fileRow
    138         if err := rows.Scan(&r.id, &r.path); err != nil {
    139             continue
    140         }
    141         files = append(files, r)
    142     }
    143     rows.Close()
    144 
    145     if err := rows.Err(); err != nil {
    146         return 0, err
    147     }
    148 
    149     for _, f := range files {
    150         computeProperties(f.id, f.path)
    151     }
    152     return len(files), nil
    153 }
    154 
    155 func getPropertyNav() (map[string][]PropertyDisplay, error) {
    156 	rows, err := db.Query(`
    157 		SELECT key, value, COUNT(*) as cnt
    158 		FROM file_properties
    159 		GROUP BY key, value
    160 		ORDER BY key, value
    161 	`)
    162 	if err != nil {
    163 		return nil, err
    164 	}
    165 	defer rows.Close()
    166 
    167 	propMap := make(map[string][]PropertyDisplay)
    168 	for rows.Next() {
    169 		var key, val string
    170 		var count int
    171 		if err := rows.Scan(&key, &val, &count); err != nil {
    172 			continue
    173 		}
    174 		propMap[key] = append(propMap[key], PropertyDisplay{Value: val, Count: count})
    175 	}
    176 	return propMap, nil
    177 }
    178 
    179 func propertyFilterHandler(w http.ResponseWriter, r *http.Request) {
    180 	trimmed := strings.TrimPrefix(r.URL.Path, "/property/")
    181 	parts := strings.SplitN(trimmed, "/", 2)
    182 	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
    183 		renderError(w, "Invalid property filter path", http.StatusBadRequest)
    184 		return
    185 	}
    186 
    187 	key := parts[0]
    188 	value := parts[1]
    189 
    190 	page := pageFromRequest(r)
    191 	perPage := perPageFromConfig(50)
    192 
    193 	var total int
    194 	err := db.QueryRow(`
    195 		SELECT COUNT(DISTINCT f.id)
    196 		FROM files f
    197 		JOIN file_properties fp ON fp.file_id = f.id
    198 		WHERE fp.key = ? AND fp.value = ?
    199 	`, key, value).Scan(&total)
    200 	if err != nil {
    201 		renderError(w, "Failed to count files", http.StatusInternalServerError)
    202 		return
    203 	}
    204 
    205 	offset := (page - 1) * perPage
    206 	files, err := queryFilesWithTags(`
    207 		SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
    208 		FROM files f
    209 		JOIN file_properties fp ON fp.file_id = f.id
    210 		WHERE fp.key = ? AND fp.value = ?
    211 		ORDER BY f.id DESC
    212 		LIMIT ? OFFSET ?
    213 	`, key, value, perPage, offset)
    214 	if err != nil {
    215 		renderError(w, "Failed to fetch files", http.StatusInternalServerError)
    216 		return
    217 	}
    218 
    219 	breadcrumbs := []Breadcrumb{
    220 		{Name: "home", URL: "/"},
    221 		{Name: "properties", URL: "/properties"},
    222 		{Name: key, URL: "/properties#prop-" + key},
    223 		{Name: value, URL: r.URL.Path},
    224 	}
    225 
    226 	title := fmt.Sprintf("%s: %s", key, value)
    227 	pageData := buildPageDataWithPagination(title, ListData{
    228 		Tagged:      files,
    229 		Breadcrumbs: breadcrumbs,
    230 	}, page, total, perPage, r)
    231 	pageData.Breadcrumbs = breadcrumbs
    232 
    233 	renderTemplate(w, "list.html", pageData)
    234 }
    235 
    236 func propertiesIndexHandler(w http.ResponseWriter, r *http.Request) {
    237 	pageData := buildPageData("Properties", nil)
    238 	pageData.Data = pageData.Properties
    239 	renderTemplate(w, "properties.html", pageData)
    240 }
    241 
    242 func handleComputeProperties(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) {
    243 	count, err := computeMissingProperties()
    244 	data := currentAdminState(r, orphanData, missingThumbnails)
    245 	if err != nil {
    246 		data.Error = "Property computation failed: " + err.Error()
    247 	} else {
    248 		data.Success = fmt.Sprintf("Computed properties for %d files.", count)
    249 	}
    250 	renderAdminPage(w, r, data)
    251 }