package handlers import ( vimeopkg "Yimaru-Backend/internal/pkgs/vimeo" "context" "encoding/json" "fmt" "io" "mime" "net/http" "net/url" "path" "sort" "strings" "time" ) type sourceFetchResult struct { FileName string ContentType string Data []byte } func normalizeSourceURL(raw string, mediaType string) (string, error) { u, err := url.Parse(strings.TrimSpace(raw)) if err != nil { return "", fmt.Errorf("invalid source URL: %w", err) } if u.Scheme != "http" && u.Scheme != "https" { return "", fmt.Errorf("source URL must use http or https") } host := strings.ToLower(u.Hostname()) if strings.Contains(host, "drive.google.com") { parts := strings.Split(strings.Trim(u.Path, "/"), "/") // Convert /file/d/{id}/view links to a direct-download endpoint. if len(parts) >= 3 && parts[0] == "file" && parts[1] == "d" { fileID := parts[2] return "https://drive.google.com/uc?export=download&id=" + fileID, nil } } if mediaType == "video" && (strings.Contains(host, "vimeo.com") || strings.Contains(host, "player.vimeo.com")) { directURL, err := resolveVimeoDirectFileURL(u.String()) if err != nil { return "", err } if directURL != "" { return directURL, nil } } return u.String(), nil } func resolveVimeoDirectFileURL(vimeoURL string) (string, error) { videoID := vimeopkg.ExtractVideoID(vimeoURL) if videoID == "" { return "", fmt.Errorf("failed to extract Vimeo video id from source URL") } src, err := url.Parse(vimeoURL) if err != nil { return "", fmt.Errorf("invalid Vimeo source URL: %w", err) } configURL := "https://player.vimeo.com/video/" + videoID + "/config" if hash := src.Query().Get("h"); hash != "" { configURL += "?h=" + url.QueryEscape(hash) } req, err := http.NewRequest(http.MethodGet, configURL, nil) if err != nil { return "", fmt.Errorf("failed to build Vimeo config request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Yimaru-Backend/1.0") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("failed to fetch Vimeo config: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("Vimeo config fetch failed with status %d", resp.StatusCode) } var payload struct { Request struct { Files struct { Progressive []struct { URL string `json:"url"` Height int `json:"height"` MimeType string `json:"mime"` } `json:"progressive"` } `json:"files"` } `json:"request"` } if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return "", fmt.Errorf("failed to decode Vimeo config payload: %w", err) } if len(payload.Request.Files.Progressive) == 0 { return "", fmt.Errorf("no downloadable Vimeo progressive stream found; provide a Vimeo URL with a valid '?h=' hash or a direct file URL") } sort.Slice(payload.Request.Files.Progressive, func(i, j int) bool { return payload.Request.Files.Progressive[i].Height > payload.Request.Files.Progressive[j].Height }) return payload.Request.Files.Progressive[0].URL, nil } func fetchMediaFromSourceURL(ctx context.Context, rawSourceURL string, mediaType string, maxSize int64) (*sourceFetchResult, error) { sourceURL, err := normalizeSourceURL(rawSourceURL, mediaType) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil) if err != nil { return nil, fmt.Errorf("failed to create source request: %w", err) } req.Header.Set("User-Agent", "Yimaru-Backend/1.0") req.Header.Set("Accept", "*/*") client := &http.Client{ Timeout: 2 * time.Minute, } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch source URL: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { return nil, fmt.Errorf("source URL returned status %d", resp.StatusCode) } limited := io.LimitReader(resp.Body, maxSize+1) data, err := io.ReadAll(limited) if err != nil { return nil, fmt.Errorf("failed to read source content: %w", err) } if int64(len(data)) > maxSize { return nil, fmt.Errorf("source file exceeds size limit for media type '%s'", mediaType) } if len(data) == 0 { return nil, fmt.Errorf("source URL returned empty content") } contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) if contentType != "" { contentType = strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0])) } if contentType == "" || contentType == "application/octet-stream" { sampleLen := 512 if len(data) < sampleLen { sampleLen = len(data) } contentType = http.DetectContentType(data[:sampleLen]) } fileName := "source-upload" if cd := resp.Header.Get("Content-Disposition"); cd != "" { if _, params, parseErr := mime.ParseMediaType(cd); parseErr == nil { if fn := strings.TrimSpace(params["filename"]); fn != "" { fileName = fn } } } if fileName == "source-upload" { if parsedURL, parseErr := url.Parse(sourceURL); parseErr == nil { base := path.Base(parsedURL.Path) if base != "" && base != "." && base != "/" { fileName = base } } } return &sourceFetchResult{ FileName: fileName, ContentType: contentType, Data: data, }, nil }