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 }