tagliatelle

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

uploads.go (9269B)


      1 package app
      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(Cfg.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, Cfg.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 
    276 		if err := generateThumbnail(finalPath, Cfg.UploadDir, filepath.Base(finalPath)); err != nil {
    277 			log.Printf("Warning: could not generate thumbnail after HEVC re-encode: %v", err)
    278 		}
    279 
    280 		return finalPath, warningMsg, nil
    281 	}
    282 	if err := os.Rename(tempPath, finalPath); err != nil {
    283 		return "", "", fmt.Errorf("failed to move file: %v", err)
    284 	}
    285 
    286 	// Generate thumbnail for video files
    287 	if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".mkv" || ext == ".webm" || ext == ".m4v" {
    288 		if err := generateThumbnail(finalPath, Cfg.UploadDir, filepath.Base(finalPath)); err != nil {
    289 			log.Printf("Warning: could not generate thumbnail: %v", err)
    290 		}
    291 	}
    292 
    293 	return finalPath, "", nil
    294 }
    295 
    296 func saveFileToDatabase(filename, path string) (int64, error) {
    297 	relPath, err := filepath.Rel(Cfg.UploadDir, path)
    298 	if err != nil {
    299 		return 0, fmt.Errorf("failed to compute relative path: %v", err)
    300 	}
    301 
    302 	res, err := DB.Exec("INSERT INTO files (filename, path, description) VALUES (?, ?, '')", filename, relPath)
    303 	if err != nil {
    304 		return 0, fmt.Errorf("failed to save file to database: %v", err)
    305 	}
    306 	id, err := res.LastInsertId()
    307 	if err != nil {
    308 		return 0, fmt.Errorf("failed to get inserted ID: %v", err)
    309 	}
    310 	computeProperties(id, path)
    311 	return id, nil
    312 }
    313 
    314 func detectVideoCodec(filePath string) (string, error) {
    315 	cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0",
    316 		"-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", filePath)
    317 	out, err := cmd.Output()
    318 	if err != nil {
    319 		return "", fmt.Errorf("failed to probe video codec: %v", err)
    320 	}
    321 	return strings.TrimSpace(string(out)), nil
    322 }
    323 
    324 func reencodeHEVCToH264(inputPath, outputPath string) error {
    325 	cmd := exec.Command("ffmpeg", "-i", inputPath,
    326 		"-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23",
    327 		"-c:a", "aac", "-movflags", "+faststart", outputPath)
    328 	cmd.Stderr = os.Stderr
    329 	cmd.Stdout = os.Stdout
    330 	return cmd.Run()
    331 }