fonchain-fiee/pkg/service/cast/ayrshare.go
2025-12-17 14:45:15 +08:00

316 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cast
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"fonchain-fiee/pkg/utils"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
)
const (
// Ayrshare API 配置
ayrshareAPIBaseURL = "https://api.ayrshare.com/api"
maxSmallFileSize = 30 * 1024 * 1024 // 30MB
apiKey = "208CBE9F-8E4F426A-990A0184-2C6287B9"
)
// UploadMediaResponse 上传媒体文件响应
type UploadMediaResponse struct {
ID string `json:"id"`
URL string `json:"url"`
FileName string `json:"fileName"`
Description string `json:"description"`
IsAs bool `json:"isAs"`
}
// UploadURLResponse 获取上传URL响应
type UploadURLResponse struct {
AccessURL string `json:"accessUrl"`
ContentType string `json:"contentType"`
UploadURL string `json:"uploadUrl"`
}
// VerifyURLResponse 验证URL响应
type VerifyURLResponse struct {
Status string `json:"status"`
StatusCode int `json:"statusCode"`
ContentType string `json:"contentType"`
}
// UploadMediaByURL 根据文件大小自动选择上传方式
// fileURL: 文件的URL链接
// apiKey: Ayrshare API Key
// fileName: 文件名(可选)
// description: 文件描述(可选)
func UploadMediaByURL(ctx context.Context, fileURL, fileName, description string) (*UploadMediaResponse, error) {
if fileURL == "" {
return nil, errors.New("文件URL不能为空")
}
ok, err := VerifyMediaURL(ctx, fileURL)
if err != nil {
return nil, err
}
if ok {
return &UploadMediaResponse{
ID: "",
URL: fileURL,
FileName: "",
Description: "",
IsAs: false,
}, nil
}
// 下载文件到临时目录
tempFile, fileSize, err := downloadFile(ctx, fileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %v", err)
}
defer os.Remove(tempFile) // 清理临时文件
zap.L().Info("文件下载完成", zap.String("tempFile", tempFile), zap.Int64("fileSize", fileSize))
// 如果没有提供文件名从URL中提取
if fileName == "" {
fileName = filepath.Base(fileURL)
}
// 根据文件大小选择上传方式
if fileSize < maxSmallFileSize {
zap.L().Info("使用小文件上传方式", zap.Int64("fileSize", fileSize))
return uploadSmallMedia(ctx, tempFile, apiKey, fileName, description)
}
zap.L().Info("使用大文件上传方式", zap.Int64("fileSize", fileSize))
return uploadLargeMedia(ctx, tempFile, apiKey, fileName, description)
}
// downloadFile 下载文件到临时目录
func downloadFile(ctx context.Context, fileURL string) (string, int64, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
if err != nil {
return "", 0, err
}
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", 0, fmt.Errorf("下载文件失败HTTP状态码: %d", resp.StatusCode)
}
// 创建临时文件
tempFile, err := os.CreateTemp("", "ayrshare_upload_*")
if err != nil {
return "", 0, err
}
defer tempFile.Close()
// 复制内容到临时文件
fileSize, err := io.Copy(tempFile, resp.Body)
if err != nil {
os.Remove(tempFile.Name())
return "", 0, err
}
return tempFile.Name(), fileSize, nil
}
// uploadSmallMedia 上传小于30MB的文件使用multipart form-data
func uploadSmallMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
// 创建multipart form-data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加文件
part, err := writer.CreateFormFile("file", fileName)
if err != nil {
return nil, err
}
if _, err := io.Copy(part, file); err != nil {
return nil, err
}
// 添加其他字段
if fileName != "" {
_ = writer.WriteField("fileName", fileName)
}
if description != "" {
_ = writer.WriteField("description", description)
}
if err := writer.Close(); err != nil {
return nil, err
}
// 发送请求
url := fmt.Sprintf("%s/media/upload", ayrshareAPIBaseURL)
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
if resp.StatusCode != http.StatusOK {
zap.L().Error("上传小文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("上传失败: HTTP %d, %s", resp.StatusCode, string(respBody))
}
var result UploadMediaResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
zap.L().Info("小文件上传成功", zap.Any("response", result))
return &result, nil
}
// uploadLargeMedia 上传大于30MB的文件使用presigned URL
func uploadLargeMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
// 获取文件的content type
ext := filepath.Ext(fileName)
if len(ext) > 0 {
ext = ext[1:] // 去掉点号
}
// Step 1: 获取上传URL
uploadURLResp, err := getUploadURL(ctx, apiKey, fileName, ext)
if err != nil {
return nil, fmt.Errorf("获取上传URL失败: %v", err)
}
zap.L().Info("获取上传URL成功", zap.Any("uploadURLResp", uploadURLResp))
// Step 2: 上传文件到presigned URL
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", uploadURLResp.UploadURL, file)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", uploadURLResp.ContentType)
client := &http.Client{Timeout: 30 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("上传文件失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
zap.L().Error("上传大文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("上传失败: HTTP %d", resp.StatusCode)
}
zap.L().Info("大文件上传成功")
// 返回结果
return &UploadMediaResponse{
URL: uploadURLResp.AccessURL,
FileName: fileName,
Description: description,
}, nil
}
// getUploadURL 获取大文件上传的presigned URL
func getUploadURL(ctx context.Context, apiKey, fileName, contentType string) (*UploadURLResponse, error) {
url := fmt.Sprintf("%s/media/uploadUrl?fileName=%s", ayrshareAPIBaseURL, fileName)
if contentType != "" {
url = fmt.Sprintf("%s&contentType=%s", url, contentType)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
zap.L().Error("获取上传URL失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("获取上传URL失败: HTTP %d, %s", resp.StatusCode, string(respBody))
}
var result UploadURLResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// VerifyMediaURL 验证媒体URL是否有效
func VerifyMediaURL(ctx context.Context, mediaURL string) (bool, error) {
url := fmt.Sprintf("%s/media/urlExists", ayrshareAPIBaseURL)
requestBody := map[string]string{
"mediaUrl": mediaURL,
}
jsonData, _ := json.Marshal(requestBody)
code, postBody, err := utils.PostBytesHeader(url, map[string]interface{}{
"Content-Type": "application/json",
"Authorization": "Bearer " + apiKey,
}, jsonData)
if err != nil {
zap.L().Error("VerifyMediaURL 提交异常", zap.Error(err))
return false, errors.New("验证链接提交异常")
}
if code != http.StatusOK {
return false, nil
}
var result *VerifyURLResponse
_ = json.Unmarshal(postBody, &result)
if result.StatusCode != http.StatusOK || result.Status != "success" {
return false, nil
}
return true, nil
}