191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
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
|
|
}
|