diff --git a/pkg/service/cast/ayrshare.go b/pkg/service/cast/ayrshare.go new file mode 100644 index 0000000..1f76291 --- /dev/null +++ b/pkg/service/cast/ayrshare.go @@ -0,0 +1,315 @@ +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 +} diff --git a/pkg/service/cast/test.go b/pkg/service/cast/test.go index 7181fc5..743dda1 100644 --- a/pkg/service/cast/test.go +++ b/pkg/service/cast/test.go @@ -99,6 +99,26 @@ func Test(ctx *gin.Context) { service.Success(ctx, resp) return } + if action == "upload" { + fileUrl := ctx.PostForm("fileUrl") + uploadResp, err := UploadMediaByURL(ctx, fileUrl, "", "") + if err != nil { + service.Error(ctx, err) + return + } + service.Success(ctx, uploadResp) + return + } + if action == "getUrl" { + fileUrl := ctx.PostForm("fileUrl") + uploadResp, err := VerifyMediaURL(ctx, fileUrl) + if err != nil { + service.Error(ctx, err) + return + } + service.Success(ctx, uploadResp) + return + } service.Success(ctx, "unknow") return } diff --git a/pkg/service/cast/work.go b/pkg/service/cast/work.go index 417f88f..3f5abba 100644 --- a/pkg/service/cast/work.go +++ b/pkg/service/cast/work.go @@ -353,11 +353,37 @@ func PostAS(workUuids []string) (errs []error) { var isVideo bool if workDetail.WorkCategory == 1 { isVideo = false - mediaUrls = workDetail.Images + for _, imageUrl := range workDetail.Images { + urlResp, err := UploadMediaByURL(context.Background(), imageUrl, "", "") + if err != nil { + zap.L().Error("Publish UploadMediaByURL failed", zap.String("imageUrl", imageUrl), zap.Error(err)) + continue + } + mediaUrls = append(mediaUrls, urlResp.URL) + } + //mediaUrls = workDetail.Images } + var coverUrl string if workDetail.WorkCategory == 2 { isVideo = true - mediaUrls = []string{workDetail.VideoUrl} + //mediaUrls = []string{workDetail.VideoUrl} + urlResp, err := UploadMediaByURL(context.Background(), workDetail.VideoUrl, "", "") + if err != nil { + zap.L().Error("Publish UploadMediaByURL failed", zap.String("VideoUrl", workDetail.VideoUrl), zap.Error(err)) + continue + } + mediaUrls = []string{urlResp.URL} + if workDetail.CoverUrl != "" { + urlResp, err = UploadMediaByURL(context.Background(), workDetail.VideoUrl, "", "") + if err != nil { + zap.L().Error("Publish UploadMediaByURL failed", zap.String("VideoUrl", workDetail.VideoUrl), zap.Error(err)) + continue + } + coverUrl = urlResp.URL + } + } + if len(mediaUrls) == 0 { + continue } ArtistInfoResp, _err := service.CastProvider.GetArtist(context.Background(), &cast.GetArtistReq{ ArtistUuid: workDetail.ArtistUuid, @@ -370,7 +396,6 @@ func PostAS(workUuids []string) (errs []error) { errs = append(errs, errors.New("艺人平台信息未配置")) continue } - for _, platformID := range needPlatformIDs { var postResp *aryshare.PostResponse = &aryshare.PostResponse{} postReq := &aryshare.PostRequest{ @@ -389,7 +414,7 @@ func PostAS(workUuids []string) (errs []error) { postReq.InstagramOptions = &aryshare.InstagramOptions{ ShareReelsFeed: false, AudioName: "", - ThumbNail: workDetail.CoverUrl, + ThumbNail: coverUrl, ThumbNailOffset: 0, Stories: false, AltText: nil, diff --git a/pkg/utils/files.go b/pkg/utils/files.go new file mode 100644 index 0000000..16b4d46 --- /dev/null +++ b/pkg/utils/files.go @@ -0,0 +1,44 @@ +package utils + +import ( + "errors" + "fmt" + "fonchain-fiee/pkg/e" + "io" + "net/http" + "os" + "strings" + + "go.uber.org/zap" +) + +// SaveUrlFileDisk 保存图片到本地 +func SaveUrlFileDisk(url string, path string, filename string) (fullPath string, err error) { + if err = CreateDirPath(path); err != nil { + zap.L().Error("SaveUrlFileDisk err ", zap.Error(err)) + return + } + if filename == "" { + stepName := strings.Split(url, "/") + if len(stepName) > 1 { + filename = stepName[len(stepName)-1] + } + } + + resp, err := http.Get(url) + if err != nil { + err = errors.New(e.GetMsg(e.ERROR_DOWNLOAD_FILE)) + return + } + defer func() { + if err := recover(); err != nil { + + } + resp.Body.Close() + }() + bytes, err := io.ReadAll(resp.Body) + fullPath = fmt.Sprintf("%s/%s", path, filename) + // 写入数据 + err = os.WriteFile(fullPath, bytes, 0777) + return +} diff --git a/pkg/utils/http.go b/pkg/utils/http.go index cab0f0e..3101f12 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -133,3 +133,24 @@ func GetUrl(apiUrl string, headerData map[string]string, proxyURL ...string) (st zap.L().Info("Get", zap.Any("url", apiUrl), zap.Any("body", body)) return } + +func PostBytesHeader(url string, header map[string]interface{}, data []byte) (int, []byte, error) { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + return 0, nil, fmt.Errorf("创建请求失败: %v", "") + } + for k, v := range header { + req.Header.Set(k, fmt.Sprintf("%v", v)) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return 0, nil, fmt.Errorf("请求失败: %v", "") + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, nil, fmt.Errorf("读取响应失败: %v", "") + } + return resp.StatusCode, body, nil +} diff --git a/pkg/utils/untils.go b/pkg/utils/untils.go index e3a6660..18437ef 100644 --- a/pkg/utils/untils.go +++ b/pkg/utils/untils.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "time" @@ -163,3 +164,11 @@ func CopyFile(src, dstDir string) (string, error) { } return dst, nil } + +func FileNameFromURL(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + return path.Base(u.Path), nil +}