tagliatelle

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

include-uploads.go (9184B)


      1 package main
      2 
      3 import (
      4     "fmt"
      5     "io"
      6     "log"
      7     "net/http"
      8     "net/url"
      9     "os"
     10     "os/exec"
     11     "path/filepath"
     12     "strconv"
     13     "strings"
     14 )
     15 
     16 func uploadHandler(w http.ResponseWriter, r *http.Request) {
     17 	if r.Method == http.MethodGet {
     18 		pageData := buildPageData("Add File", nil)
     19 		renderTemplate(w, "add.html", pageData)
     20 		return
     21 	}
     22 
     23 	// Parse the multipart form (with max memory limit, e.g., 32MB)
     24 	err := r.ParseMultipartForm(32 << 20)
     25 	if err != nil {
     26 		renderError(w, "Failed to parse form", http.StatusBadRequest)
     27 		return
     28 	}
     29 
     30 	files := r.MultipartForm.File["file"]
     31 	if len(files) == 0 {
     32 		renderError(w, "No files uploaded", http.StatusBadRequest)
     33 		return
     34 	}
     35 
     36 	var warnings []string
     37 	var lastID int64
     38 
     39 	// Process each file
     40 	for _, fileHeader := range files {
     41 		file, err := fileHeader.Open()
     42 		if err != nil {
     43 			renderError(w, "Failed to open uploaded file", http.StatusInternalServerError)
     44 			return
     45 		}
     46 		defer file.Close()
     47 
     48 		id, warningMsg, err := processUpload(file, fileHeader.Filename)
     49 		if err != nil {
     50 			renderError(w, err.Error(), http.StatusInternalServerError)
     51 			return
     52 		}
     53 
     54 		lastID = id
     55 
     56 		if warningMsg != "" {
     57 			warnings = append(warnings, warningMsg)
     58 		}
     59 	}
     60 
     61 	var warningMsg string
     62 	if len(warnings) > 0 {
     63 		warningMsg = strings.Join(warnings, "; ")
     64 	}
     65 
     66 	redirectTarget := "/untagged"
     67 	if len(files) == 1 && lastID != 0 {
     68 		redirectTarget = "/file/" + strconv.FormatInt(lastID, 10)
     69 	}
     70 
     71 	redirectWithWarning(w, r, redirectTarget, warningMsg)
     72 }
     73 
     74 func processUpload(src io.Reader, filename string) (int64, string, error) {
     75     finalFilename, finalPath, conflictID, err := checkFileConflictStrict(filename)
     76     if err != nil {
     77         return 0, "", err
     78     }
     79     if conflictID != 0 {
     80         return conflictID, "", nil
     81     }
     82 
     83     tempPath := finalPath + ".tmp"
     84     tempFile, err := os.Create(tempPath)
     85     if err != nil {
     86         return 0, "", fmt.Errorf("failed to create temp file: %v", err)
     87     }
     88 
     89     _, err = io.Copy(tempFile, src)
     90     tempFile.Close()
     91     if err != nil {
     92         os.Remove(tempPath)
     93         return 0, "", fmt.Errorf("failed to copy file data: %v", err)
     94     }
     95 
     96     ext := strings.ToLower(filepath.Ext(filename))
     97     videoExts := map[string]bool{
     98         ".mp4": true, ".mov": true, ".avi": true,
     99         ".mkv": true, ".webm": true, ".m4v": true,
    100     }
    101 
    102     var processedPath string
    103     var warningMsg string
    104 
    105     if videoExts[ext] || ext == ".cbz" {
    106         // Process videos and CBZ files
    107         processedPath, warningMsg, err = processVideoFile(tempPath, finalPath)
    108         if err != nil {
    109             os.Remove(tempPath)
    110             return 0, "", err
    111         }
    112     } else {
    113         // Non-video, non-CBZ → just rename temp file to final
    114         if err := os.Rename(tempPath, finalPath); err != nil {
    115             return 0, "", fmt.Errorf("failed to move file: %v", err)
    116         }
    117         processedPath = finalPath
    118     }
    119 
    120     id, err := saveFileToDatabase(finalFilename, processedPath)
    121     if err != nil {
    122         os.Remove(processedPath)
    123         return 0, "", err
    124     }
    125 
    126     return id, warningMsg, nil
    127 }
    128 
    129 func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
    130 	if r.Method != http.MethodPost {
    131 		http.Redirect(w, r, "/upload", http.StatusSeeOther)
    132 		return
    133 	}
    134 
    135 	fileURL := r.FormValue("fileurl")
    136 	if fileURL == "" {
    137 		renderError(w, "No URL provided", http.StatusBadRequest)
    138 		return
    139 	}
    140 
    141 	customFilename := strings.TrimSpace(r.FormValue("filename"))
    142 
    143 	parsedURL, err := url.ParseRequestURI(fileURL)
    144 	if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
    145 		renderError(w, "Invalid URL", http.StatusBadRequest)
    146 		return
    147 	}
    148 
    149 	resp, err := http.Get(fileURL)
    150 	if err != nil || resp.StatusCode != http.StatusOK {
    151 		renderError(w, "Failed to download file", http.StatusBadRequest)
    152 		return
    153 	}
    154 	defer resp.Body.Close()
    155 
    156 	var filename string
    157 	urlExt := filepath.Ext(parsedURL.Path)
    158 	if customFilename != "" {
    159 		filename = customFilename
    160 		if filepath.Ext(filename) == "" && urlExt != "" {
    161 			filename += urlExt
    162 		}
    163 	} else {
    164 		parts := strings.Split(parsedURL.Path, "/")
    165 		filename = parts[len(parts)-1]
    166 		if filename == "" {
    167 			filename = "file_from_url"
    168 		}
    169 	}
    170 
    171 	id, warningMsg, err := processUpload(resp.Body, filename)
    172 	if err != nil {
    173 		renderError(w, err.Error(), http.StatusInternalServerError)
    174 		return
    175 	}
    176 
    177 	redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg)
    178 }
    179 
    180 func ytdlpHandler(w http.ResponseWriter, r *http.Request) {
    181 	if r.Method != http.MethodPost {
    182 		http.Redirect(w, r, "/upload", http.StatusSeeOther)
    183 		return
    184 	}
    185 
    186 	videoURL := r.FormValue("url")
    187 	if videoURL == "" {
    188 		renderError(w, "No URL provided", http.StatusBadRequest)
    189 		return
    190 	}
    191 
    192 	outTemplate := filepath.Join(config.UploadDir, "%(title)s.%(ext)s")
    193 	filenameCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, "--get-filename", videoURL)
    194 	filenameBytes, err := filenameCmd.Output()
    195 	if err != nil {
    196 		renderError(w, fmt.Sprintf("Failed to get filename: %v", err), http.StatusInternalServerError)
    197 		return
    198 	}
    199 	expectedFullPath := strings.TrimSpace(string(filenameBytes))
    200 	expectedFilename := filepath.Base(expectedFullPath)
    201 
    202 	finalFilename, finalPath, conflictID, err := checkFileConflictStrict(expectedFilename)
    203 	if err != nil {
    204 		renderError(w, err.Error(), http.StatusConflict)
    205 		return
    206 	}
    207 	if conflictID != 0 {
    208 		redirectWithWarning(w, r, fmt.Sprintf("/file/%d", conflictID), "")
    209 		return
    210 	}
    211 
    212 	downloadCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, videoURL)
    213 	downloadCmd.Stdout = os.Stdout
    214 	downloadCmd.Stderr = os.Stderr
    215 	if err := downloadCmd.Run(); err != nil {
    216 		renderError(w, fmt.Sprintf("Failed to download video: %v", err), http.StatusInternalServerError)
    217 		return
    218 	}
    219 
    220 	if expectedFullPath != finalPath {
    221 		if err := os.Rename(expectedFullPath, finalPath); err != nil {
    222 			renderError(w, fmt.Sprintf("Failed to move downloaded file: %v", err), http.StatusInternalServerError)
    223 			return
    224 		}
    225 	}
    226 
    227 	tempPath := finalPath + ".tmp"
    228 	if err := os.Rename(finalPath, tempPath); err != nil {
    229 		renderError(w, fmt.Sprintf("Failed to create temp file for processing: %v", err), http.StatusInternalServerError)
    230 		return
    231 	}
    232 
    233 	processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
    234 	if err != nil {
    235 		os.Remove(tempPath)
    236 		renderError(w, fmt.Sprintf("Failed to process video: %v", err), http.StatusInternalServerError)
    237 		return
    238 	}
    239 
    240 	id, err := saveFileToDatabase(finalFilename, processedPath)
    241 	if err != nil {
    242 		os.Remove(processedPath)
    243 		renderError(w, err.Error(), http.StatusInternalServerError)
    244 		return
    245 	}
    246 
    247 	redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg)
    248 }
    249 
    250 func processVideoFile(tempPath, finalPath string) (string, string, error) {
    251 	ext := strings.ToLower(filepath.Ext(finalPath))
    252 
    253 	// Handle CBZ files
    254 	if ext == ".cbz" {
    255 		if err := os.Rename(tempPath, finalPath); err != nil {
    256 			return "", "", fmt.Errorf("failed to move file: %v", err)
    257 		}
    258 		if err := generateCBZThumbnail(finalPath, config.UploadDir, filepath.Base(finalPath)); err != nil {
    259 			log.Printf("Warning: could not generate CBZ thumbnail: %v", err)
    260 		}
    261 		return finalPath, "", nil
    262 	}
    263 
    264 	// Handle video files
    265 	codec, err := detectVideoCodec(tempPath)
    266 	if err != nil {
    267 		return "", "", err
    268 	}
    269 	if codec == "hevc" || codec == "h265" {
    270 		warningMsg := "The video uses HEVC and has been re-encoded to H.264 for browser compatibility."
    271 		if err := reencodeHEVCToH264(tempPath, finalPath); err != nil {
    272 			return "", "", fmt.Errorf("failed to re-encode HEVC video: %v", err)
    273 		}
    274 		os.Remove(tempPath)
    275 		return finalPath, warningMsg, nil
    276 	}
    277 	if err := os.Rename(tempPath, finalPath); err != nil {
    278 		return "", "", fmt.Errorf("failed to move file: %v", err)
    279 	}
    280 
    281 	// Generate thumbnail for video files
    282 	if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".mkv" || ext == ".webm" || ext == ".m4v" {
    283 		if err := generateThumbnail(finalPath, config.UploadDir, filepath.Base(finalPath)); err != nil {
    284 			log.Printf("Warning: could not generate thumbnail: %v", err)
    285 		}
    286 	}
    287 
    288 	return finalPath, "", nil
    289 }
    290 
    291 func saveFileToDatabase(filename, path string) (int64, error) {
    292 	res, err := db.Exec("INSERT INTO files (filename, path, description) VALUES (?, ?, '')", filename, path)
    293 	if err != nil {
    294 		return 0, fmt.Errorf("failed to save file to database: %v", err)
    295 	}
    296 	id, err := res.LastInsertId()
    297 	if err != nil {
    298 		return 0, fmt.Errorf("failed to get inserted ID: %v", err)
    299 	}
    300 	computeProperties(id, path)
    301 	return id, nil
    302 }
    303 
    304 
    305 func detectVideoCodec(filePath string) (string, error) {
    306 	cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0",
    307 		"-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", filePath)
    308 	out, err := cmd.Output()
    309 	if err != nil {
    310 		return "", fmt.Errorf("failed to probe video codec: %v", err)
    311 	}
    312 	return strings.TrimSpace(string(out)), nil
    313 }
    314 
    315 func reencodeHEVCToH264(inputPath, outputPath string) error {
    316 	cmd := exec.Command("ffmpeg", "-i", inputPath,
    317 		"-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23",
    318 		"-c:a", "aac", "-movflags", "+faststart", outputPath)
    319 	cmd.Stderr = os.Stderr
    320 	cmd.Stdout = os.Stdout
    321 	return cmd.Run()
    322 }