include-admin-thumbnails.go (6460B)
1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "net/http" 8 "net/url" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 ) 14 15 type VideoFile struct { 16 ID int 17 Filename string 18 Path string 19 HasThumbnail bool 20 ThumbnailPath string 21 EscapedFilename string 22 } 23 24 func generateThumbnailAtTime(videoPath, uploadDir, filename, timestamp string) error { 25 thumbDir := filepath.Join(uploadDir, "thumbnails") 26 if err := os.MkdirAll(thumbDir, 0755); err != nil { 27 return fmt.Errorf("failed to create thumbnails directory: %v", err) 28 } 29 30 thumbPath := filepath.Join(thumbDir, filename+".jpg") 31 32 var stderr bytes.Buffer 33 cmd := exec.Command("ffmpeg", "-y", "-ss", timestamp, "-i", videoPath, "-vframes", "1", "-vf", "scale=400:-1", thumbPath) 34 cmd.Stderr = &stderr 35 36 if err := cmd.Run(); err != nil { 37 return fmt.Errorf("failed to generate thumbnail at %s: %v\nffmpeg output: %s", timestamp, err, stderr.String()) 38 } 39 40 return nil 41 } 42 43 func getVideoFiles() ([]VideoFile, error) { 44 videoExts := []string{".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"} 45 46 rows, err := db.Query(`SELECT id, filename, path FROM files ORDER BY id DESC`) 47 if err != nil { 48 return nil, err 49 } 50 defer rows.Close() 51 52 var videos []VideoFile 53 for rows.Next() { 54 var v VideoFile 55 if err := rows.Scan(&v.ID, &v.Filename, &v.Path); err != nil { 56 log.Printf("Warning: getVideoFiles: failed to scan row: %v", err) 57 continue 58 } 59 60 // Check if it's a video file 61 isVideo := false 62 ext := strings.ToLower(filepath.Ext(v.Filename)) 63 for _, vidExt := range videoExts { 64 if ext == vidExt { 65 isVideo = true 66 break 67 } 68 } 69 70 if !isVideo { 71 continue 72 } 73 74 v.EscapedFilename = url.PathEscape(v.Filename) 75 thumbPath := filepath.Join(config.UploadDir, "thumbnails", v.Filename+".jpg") 76 v.ThumbnailPath = "/uploads/thumbnails/" + v.EscapedFilename + ".jpg" 77 78 if _, err := os.Stat(thumbPath); err == nil { 79 v.HasThumbnail = true 80 } 81 82 videos = append(videos, v) 83 } 84 85 return videos, nil 86 } 87 88 func thumbnailsHandler(w http.ResponseWriter, r *http.Request) { 89 allVideos, err := getVideoFiles() 90 if err != nil { 91 log.Printf("Error: thumbnailsHandler: failed to get video files: %v", err) 92 renderError(w, "Failed to get video files: "+err.Error(), http.StatusInternalServerError) 93 return 94 } 95 96 missing, err := getMissingThumbnailVideos() 97 if err != nil { 98 log.Printf("Error: thumbnailsHandler: failed to get missing thumbnail videos: %v", err) 99 renderError(w, "Failed to get video files: "+err.Error(), http.StatusInternalServerError) 100 return 101 } 102 103 pageData := buildPageData("Thumbnail Management", struct { 104 AllVideos []VideoFile 105 MissingThumbnails []VideoFile 106 Error string 107 Success string 108 }{ 109 AllVideos: allVideos, 110 MissingThumbnails: missing, 111 Error: r.URL.Query().Get("error"), 112 Success: r.URL.Query().Get("success"), 113 }) 114 115 renderTemplate(w, "thumbnails.html", pageData) 116 } 117 118 func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) { 119 if r.Method != http.MethodPost { 120 http.Redirect(w, r, "/admin", http.StatusSeeOther) 121 return 122 } 123 124 action := r.FormValue("action") 125 redirectTo := r.FormValue("redirect") 126 if redirectTo == "" { 127 redirectTo = "thumbnails" 128 } 129 130 redirectBase := "/" + redirectTo 131 132 switch action { 133 case "generate_all": 134 missing, err := getMissingThumbnailVideos() 135 if err != nil { 136 log.Printf("Error: generateThumbnailHandler: failed to get missing thumbnails: %v", err) 137 http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("Failed to get videos: "+err.Error()), http.StatusSeeOther) 138 return 139 } 140 141 successCount := 0 142 var errors []string 143 144 for _, v := range missing { 145 if err := generateThumbnail(v.Path, config.UploadDir, v.Filename); err != nil { 146 log.Printf("Error: generateThumbnailHandler: failed to generate thumbnail for %s: %v", v.Filename, err) 147 errors = append(errors, fmt.Sprintf("%s: %v", v.Filename, err)) 148 } else { 149 successCount++ 150 } 151 } 152 153 if len(errors) > 0 { 154 http.Redirect(w, r, redirectBase+"?success="+url.QueryEscape(fmt.Sprintf("Generated %d thumbnails", successCount))+"&error="+url.QueryEscape(fmt.Sprintf("Failed: %s", strings.Join(errors, "; "))), http.StatusSeeOther) 155 } else { 156 http.Redirect(w, r, redirectBase+"?success="+url.QueryEscape(fmt.Sprintf("Successfully generated %d thumbnails", successCount)), http.StatusSeeOther) 157 } 158 159 case "generate_single": 160 fileID := r.FormValue("file_id") 161 timestamp := strings.TrimSpace(r.FormValue("timestamp")) 162 163 if timestamp == "" { 164 timestamp = "00:00:05" 165 } 166 167 var filename, path string 168 err := db.QueryRow("SELECT filename, path FROM files WHERE id=?", fileID).Scan(&filename, &path) 169 if err != nil { 170 log.Printf("Error: generateThumbnailHandler: file not found for id=%s: %v", fileID, err) 171 http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("File not found"), http.StatusSeeOther) 172 return 173 } 174 175 if !filepath.IsAbs(path) { 176 path = filepath.Join(config.UploadDir, path) 177 } 178 179 if err = generateThumbnailAtTime(path, config.UploadDir, filename, timestamp); err != nil { 180 log.Printf("Error: generateThumbnailHandler: failed to generate thumbnail for file id=%s at %s: %v", fileID, timestamp, err) 181 http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("Failed to generate thumbnail: "+err.Error()), http.StatusSeeOther) 182 return 183 } 184 185 if redirectTo == "admin" { 186 http.Redirect(w, r, "/admin?success="+url.QueryEscape(fmt.Sprintf("Thumbnail generated for file %s at %s", fileID, timestamp)), http.StatusSeeOther) 187 } else { 188 http.Redirect(w, r, fmt.Sprintf("/file/%s?success=%s", fileID, url.QueryEscape(fmt.Sprintf("Thumbnail generated at %s", timestamp))), http.StatusSeeOther) 189 } 190 191 default: 192 http.Redirect(w, r, redirectBase, http.StatusSeeOther) 193 } 194 } 195 196 func generateThumbnail(videoPath, uploadDir, filename string) error { 197 if err := generateThumbnailAtTime(videoPath, uploadDir, filename, "00:00:05"); err != nil { 198 log.Printf("Warning: generateThumbnail: seek to 5s failed for %s, retrying from start: %v", filename, err) 199 return generateThumbnailAtTime(videoPath, uploadDir, filename, "00:00:00") 200 } 201 202 return nil 203 } 204 205 func getMissingThumbnailVideos() ([]VideoFile, error) { 206 allVideos, err := getVideoFiles() 207 if err != nil { 208 return nil, err 209 } 210 211 var missing []VideoFile 212 for _, v := range allVideos { 213 if !v.HasThumbnail { 214 missing = append(missing, v) 215 } 216 } 217 218 return missing, nil 219 }