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