tagliatelle

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

include-uploads.go (8779B)


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