package updownloadctrl import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "mime" "net/http" "path" "path/filepath" "strconv" "strings" "time" "91porn-server/common" "91porn-server/common/constant" "91porn-server/common/crypt" "91porn-server/common/log" "91porn-server/common/m3u8" "91porn-server/common/stderr" "91porn-server/models/commod" "91porn-server/models/l/operatorlgmod" "91porn-server/models/v/sourcemod" "91porn-server/models/v/vidmod" "91porn-server/web/service/updownloadser" "91porn-server/web/service/vidser" "91porn-server/web/webg" "github.com/gin-gonic/gin" ) const Retries = 3 //重试3次 // buffer bytes.Buffer var interval = []int64{5, 15, 30, 60} //通知时间间隔 // Upload doc // @Summary 文件管理 - 表单上传文件 // @Description 表单上传文件 用于上传视频对应的封面图 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param upload formData file true "文件" // @Param id formData string true "文件ID" // @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.jpg"}}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /web/vid/upload [post] func Upload(c *gin.Context) { headers, err := c.FormFile("upload") if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } id := c.PostForm("id") if id == "" { common.ServeJSON(c, stderr.ErrUploadError, "id is required") return } f, err := headers.Open() if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } ext := strings.TrimLeft(filepath.Ext(headers.Filename), ".") byteData, err := io.ReadAll(f) f.Close() if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } fileData := base64.StdEncoding.EncodeToString(byteData) resp, err := updownloadser.SendVidCover2FS(id, ext, fileData) if err != nil { log.Warn("file upload wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } if resp.Code == http.StatusOK { common.ServeJSON(c, stderr.Success, resp.Data) return } common.ServeJSON(c, stderr.ErrUploadError, "") } // UploadVideo doc // @Summary 文件管理 - 上传视频 // @Description 上传视频 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param upload formData file true "文件类型" // @Success 200 {string} json "{"msg": "success" "data":""}" // @Failure 400 {string} json "{"msg": "fail"}" // @Router /web/vid/uploadVideo [post] func UploadVideo(c *gin.Context) { var mime_type = []string{"video/x-flv", "video/mp4", "application/x-mpegURL", "video/MP2T", "video/3gpp", "video/x-msvideo", "video/x-ms-wmv"} var isVideo bool var resp vidmod.AwsResport vidres := vidmod.WenUploadRes{} var httpErr error headers, err := c.FormFile("upload") if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } f, err := headers.Open() if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } defer func() { _ = f.Close() }() mtype := mime.TypeByExtension(strings.ToLower(filepath.Ext(headers.Filename))) for _, v := range mime_type { if mtype == v { isVideo = true break } } if !isVideo { common.ServeJSON(c, stderr.ErrMimeType, "") return } byteData, _ := io.ReadAll(f) md5 := crypt.ByteToMd5(byteData) fileData := base64.StdEncoding.EncodeToString(byteData) resp, httpErr = updownloadser.SendFile2FS(common.UUID(), fileData, 1, 1) if httpErr != nil { log.Warn("file upload wrong ", log.E(httpErr)) common.ServeJSON(c, stderr.ErrUploadError, httpErr.Error()) return } if resp.Code == http.StatusOK { vidres.SourceID = resp.Data.VID vidres.SourceURL = resp.Data.VideoURI vidres.Size = int(headers.Size) vidres.MD5 = md5 fileName := strconv.FormatInt(time.Now().Unix(), 10) + common.RandStrRandr(8) + path.Ext(headers.Filename) vidres.Filename = fileName vidres.MimeType = mtype common.ServeJSON(c, stderr.Success, vidres) return } common.ServeJSON(c, stderr.ErrUploadError, "") } // UploadDotStream doc // @Summary 文件管理 - 流式断点续传文件 // @Description 流式断点续传 // @Tags uploaddown // @Accept mpfd,json // @Produce json,html // @Param id formData string true "文件ID 文件MD5做ID" // @Param chunkNumber formData string true "第几片视频" // @Param totalChunks formData string true "视频总片数" // @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" // @Failure 400 {string} json "{"msg": "fail"}" // @Router /vid/uploadDotStream [post] func UploadDotStream(c *gin.Context) { var id, pos, totalPos, size string var cnt int64 id = c.PostForm("id") pos = c.PostForm("chunkNumber") totalPos = c.PostForm("totalChunks") size = c.PostForm("totalSize") var mime_type = []string{"video/x-flv", "video/mp4", "application/x-mpegURL", "video/MP2T", "video/3gpp", "video/x-msvideo", "video/x-ms-wmv"} var isVideo bool var resp vidmod.AwsResport vidres := vidmod.WenUploadRes{} if id == "" || pos == "" || totalPos == "" { common.ServeJSON(c, stderr.ErrParamError, "upload args error") return } posint, _ := strconv.ParseInt(pos, 10, 32) total, _ := strconv.ParseInt(totalPos, 10, 32) var httpErr error headers, err := c.FormFile("upload") if err != nil || headers.Filename == "" || headers.Size == 0 { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err) return } f, err := headers.Open() if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err) return } defer func() { _ = f.Close() }() mtype := mime.TypeByExtension(strings.ToLower(filepath.Ext(headers.Filename))) for _, v := range mime_type { if mtype == v { isVideo = true break } } if !isVideo { common.ServeJSON(c, stderr.ErrMimeType, "") return } var buffer bytes.Buffer _, err = buffer.ReadFrom(f) if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err) return } fileData := base64.StdEncoding.EncodeToString(buffer.Bytes()) for cnt < Retries { resp, httpErr = updownloadser.SendFile2FS(id, fileData, posint, total) if httpErr == nil { break } time.Sleep(time.Second) log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) cnt++ } if httpErr != nil { log.Warn("file upload wrong ", log.E(httpErr)) common.ServeJSON(c, stderr.ErrUploadError, nil) return } if resp.Code != http.StatusOK { common.ServeJSON(c, stderr.ErrUploadError, "") return } vidres.Size, _ = strconv.Atoi(size) vidres.SourceID = resp.Data.VID vidres.SourceURL = resp.Data.VideoURI fileName := strconv.FormatInt(time.Now().Unix(), 10) + common.RandStrRandr(8) + path.Ext(headers.Filename) vidres.Filename = fileName vidres.MimeType = mtype common.ServeJSON(c, stderr.Success, vidres) } // UploadDotJson doc // @Summary 文件管理 - API断点续传文件 // @Description API断点续传 // @Tags uploaddown // @Accept mpfd,json // @Produce json,html // @Param id formData string true "taskId 文件MD5做ID" // @Param pos formData integer true "第几片视频" // @Param totalPos formData integer true "视频总片数" // @Param data formData string true "数据内容" // @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" // @Failure 400 {string} json "{"msg": "fail"}" // @Router /vid/uploadDotJson [post] func UploadDotJson(c *gin.Context) { var cnt int var resp vidmod.AwsResport vidres := vidmod.WenUploadRes{} var httpErr error var args struct { ID string `form:"id" json:"id" binding:"required"` //taskId POS int64 `form:"pos" json:"pos" binding:"required"` //分片序号 TotalPos int64 `form:"totalPos" json:"totalPos" binding:"required"` //总分片数 Data string `form:"data" json:"data" binding:"required"` //分片内容 } if err := c.ShouldBind(&args); err != nil { common.ServeJSON(c, stderr.ErrParamError, err) return } for cnt < Retries { resp, httpErr = updownloadser.SendFile2FS(args.ID, args.Data, args.POS, args.TotalPos) if httpErr == nil { break } time.Sleep(time.Duration(interval[cnt]) * time.Second) log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) cnt++ } if httpErr != nil { log.Warn("file upload wrong ", log.E(httpErr)) common.ServeJSON(c, stderr.ErrUploadError, nil) return } if resp.Code == http.StatusOK { vidres.SourceID = resp.Data.VID vidres.SourceURL = resp.Data.VideoURI common.ServeJSON(c, stderr.Success, vidres) return } common.ServeJSON(c, stderr.ErrUploadError, resp) } // Submit doc // @Summary 提交视频的基本信息 // @Description 视频基本信息提交 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param uid formData integer true "用户ID" // @Param title formData string true "视频标题" // @Param tags formData array true "视频标签数组" // @Param playTime formData string false "视频时长" // @Param cover formData string false "封面图" // @Param coverThumb formData string false "小图" // @Param via formData string false "视频来源" // @Param coins formData integer false "观看金币" // @Param size formData integer false "文件大小" // @Param mimeType formData string false "影片类型" // @Param sourceURL formData string true "资源url" // @Param sourceID formData string true "上传视频成功后 返回的ID" // @Param filename formData string true "文件名" // @Param resolution formData string false "分辨率" // @Param md5 formData string true "文件摘要" // @Param freeTime formData integer false "免费观影时长" // @Param seedLinkUrl formData string false "种子下载链接" // @Param seedSize formData integer false "种子影片大小" // @Param seedPlayTime formData integer false "种子影片时长" // @Success 200 {string} json "{"msg": "操作成功"}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /web/vid/submit [post] func Submit(ctx *gin.Context) { manager, err := common.GetAdminAct(ctx) if err != nil { common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) return } param := vidmod.WebSubmitReq{} if err = ctx.ShouldBind(¶m); err != nil { common.ServeJSON(ctx, stderr.ErrParamError, nil) return } //对上传的视频的秒数做限制 if param.PlayTime != 0 { if param.PlayTime < vidmod.SubmitVideoPlayTimeLimit { common.ServeJSON(ctx, stderr.ErrPlayTimeTooShort, "") return } } //默认newsType if len(param.NewsType) == 0 { param.NewsType = vidmod.SP } //图集的第一张放到封面中 if (param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC) && len(param.SeriesCover) != 0 { param.Cover = param.SeriesCover[0] param.CoverThumb = param.SeriesCover[0] } code, vid := vidser.SubmitBase(param) if code != stderr.Success { _, _ = vidser.DeleteManyByTTL(vid) common.ServeJSON(ctx, code, vid) return } if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC || param.NewsType == vidmod.AD_COVER || (param.NewsType == vidmod.SEED_LINK && param.SourceURL == "") { common.ServeJSON(ctx, code, vid) return } code = vidser.SendBaseInfo2AWs(param) if code != stderr.Success { _, _ = vidser.DeleteManyByTTL(vid) } log, _ := json.Marshal(param) _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Add, string(log), ctx.Request.URL.RequestURI()) common.ServeJSON(ctx, code, vid) } // PullFileInfoFromAws doc // @Summary // @Description 从AWS拉取视频信息 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param token formData string true "调用此接口的token"" // @Param uid formData array true "用户ID 数组" // @Param cityes formData array true "城市名字 数组" // @Param maxPlayTime formData integer false "最长播放时间" // @Param minPlayTime formData integer false "最小播放时间" // @Param pageSize formData integer true "分页数据" // @Param newUpdateAt formData string false "批次更新标识" // @Success 200 {string} json "{"msg": "操作成功"}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /web/vid/pullFileInfoFromAws [post] func PullFileInfoFromAws(ctx *gin.Context) { param := vidmod.PullReq{} if err := ctx.ShouldBind(¶m); err != nil { common.ServeJSON(ctx, stderr.ErrParamError, nil) return } if param.Token != "q7ydjHVIVBYiTqQ&" { common.ServeJSON(ctx, stderr.ErrNoToken, nil) return } var code stderr.Code var data map[string]interface{} switch param.SyncType { case "SP": code, data = updownloadser.PullFile(param) case "SERIES": code, data = updownloadser.PullSeriesFile(param) } if param.NewUpdateAt != "" { updownloadser.SyncNewImportVideoToElastic(param.NewUpdateAt, time.Time{}, time.Time{}) } common.ServeJSON(ctx, code, data) } // UploadStatic doc // @Summary 文件管理 - 表单上传文件 // @Description 表单上传文件,上传静态文件到AWS 用于独立文件上传 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param upload formData file true "文件" // @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /web/vid/uploadStatic [post] func UploadStatic(c *gin.Context) { var ( resp vidmod.FsSendSingleResp commres commod.Resp err error ) headers, err := c.FormFile("upload") id := c.PostForm("id") if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } f, err := headers.Open() if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } byteData, err := io.ReadAll(f) f.Close() if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } fileData := base64.StdEncoding.EncodeToString(byteData) if id == "" { resp, err = updownloadser.SendImageToFS(headers.Filename, fileData) } else { ext := strings.TrimLeft(filepath.Ext(headers.Filename), ".") commres, err = updownloadser.SendVidCover2FS(id, ext, fileData) } if err != nil { log.Warn("file upload wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } if resp.Code == stderr.Success { common.ServeJSON(c, stderr.Success, gin.H{ "coverImg": resp.Data.FileName, "sizeB": headers.Size, //字节 }) return } if commres.Code == stderr.Success { common.ServeJSON(c, stderr.Success, commres.Data) return } common.ServeJSON(c, stderr.ErrUploadError, "") } // UploadStaticBatch doc // @Summary 文件管理 - 表单上传文件 批量上传 // @Description 表单上传文件,上传静态文件到AWS 用于独立文件上传 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param uploads formData file true "文件" // @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /web/vid/uploadStatic [post] func UploadStaticBatch(c *gin.Context) { form, err := c.MultipartForm() if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } files := form.File["upload[]"] batch := make([]*updownloadser.FileInfo, len(files)) for i, f := range files { fi, err := f.Open() if err != nil { log.Warn("headers Open file wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } byteData, err := io.ReadAll(fi) fi.Close() if err != nil { log.Warn("request multipart wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } fileData := base64.StdEncoding.EncodeToString(byteData) fileInfo := &updownloadser.FileInfo{ FileName: &f.Filename, FileData: &fileData, Resize: true, } batch[i] = fileInfo } resp, err := updownloadser.SendImageToFSBatch(updownloadser.InfoBatch{Batch: batch}) if err != nil { log.Warn("file upload wrong ", log.E(err)) common.ServeJSON(c, stderr.ErrUploadError, err.Error()) return } common.ServeJSON(c, stderr.Success, gin.H{"filePath": resp.Data.GetFileNames(), "success": resp.Data.Count()}) } // DownloadM3u8 doc // @Summary 文件管理 - 下载文件接口 // @Description 下载文件 // @Tags 正式 // @Accept mpfd,json // @Produce json,html // @Success 200 {string} json "{"msg": "操作成功"}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /vid/m3u8/:source [get] func DownloadM3u8(c *gin.Context) { var cdnURL string source := c.Param("source") if source == "" { common.ServeJSON(c, stderr.ErrParamError, "") return } if ext := filepath.Ext(source); ext != ".m3u8" { common.ServeJSON(c, stderr.ErrMimeType, "") return } fileName := filepath.Base(source) c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) //媒体资源库选择 mds := m3u8.GetMediaResouce(source) if mds == constant.MediaSourceLaoSiJi { source = strings.Replace(source, "/laosiji/", "", -1) } data, err := updownloadser.FsIO(source, mds) if err != nil { log.ZapLog.Warn("download file error", log.E(err)) c.JSON(http.StatusBadRequest, gin.H{"data": "", "mag": stderr.ErrDownloadError.Msg()}) return } if webg.Conf.Switch.CdnOriginBaseOnLocal { cdnURL = webg.Conf.URL.CdnUrl } else { cdns := sourcemod.GetCdnURL() if len(cdns) > 0 { cdnURL = cdns[0].Url } else { cdnURL = webg.Conf.URL.CdnUrl } } // TS 分片鉴权签名密钥:优先取配置(webg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量) keyVersion, authKey := webg.Conf.Base.TsAuth.Resolve() var bytebuff *bytes.Buffer if mds == constant.MediaSourcePMS { // bytebuff = m3u8.DecodeFromReader(data, "", "/api/web/admin/vid/pms/sec", webg.Conf.Base.PMCSecIV) bytebuff = m3u8.DecodeFromReader(data, "", "/api/web/admin/vid", "", mds, authKey, keyVersion, source) } else { serUrl := "/api/web/admin/vid/sp/sec" if mds == constant.MediaSourceJH1B { serUrl = "/api/web/admin/vid/sp/m3u8sec" } else if mds == constant.MediaSourceLaoSiJi { serUrl = "/api/web/admin/vid/sp/lsjsec" } bytebuff = m3u8.DecodeFromReader(data, common.BindUrl(cdnURL, strings.Trim(source, fileName)), serUrl, "", mds, authKey, keyVersion, source) } if bytebuff == nil { log.Warn("can't create m3u8 file") c.JSON(http.StatusNoContent, gin.H{"data": "", "msg": stderr.CodeEmptyData.Msg()}) return } c.Data(200, "application/octet-stream", bytebuff.Bytes()) } // SyncNewVideo doc // @Summary 文件管理 - 下载文件接口 // @Description 下载文件 // @Tags 正式 // @Accept mpfd,json // @Produce json,html // @Param newUpdateAt formData string true "视频标识" // @Success 200 {string} json "{"msg": "操作成功"}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /api/web/admin/vid/syncNewVideo [post] func SyncNewVideo(c *gin.Context) { type Info struct { NewUpdateAt string `form:"newUpdateAt" json:"newUpdateAt"` Token string `form:"token" json:"token" binging:"required"` Start time.Time `form:"start" json:"start"` End time.Time `form:"end" json:"end"` } var param = Info{} if err := c.ShouldBind(¶m); err != nil { common.ServeJSON(c, stderr.ErrParamError, err) return } if param.Token != "q7ydjHVIVBYiTqQ&" { common.ServeJSON(c, stderr.ErrNoToken, "") return } updownloadser.SyncNewImportVideoToElastic(param.NewUpdateAt, param.Start, param.End) common.ServeJSON(c, stderr.Success, "") } // SpiderSyncSubmit doc // @Summary 爬虫同步视频基本信息 // @Description 视频基本信息提交 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param uid formData integer true "用户ID" // @Param title formData string true "视频标题" // @Param tags formData array true "视频标签数组" // @Param playTime formData string false "视频时长" // @Param cover formData string false "封面图" // @Param coverThumb formData string false "小图" // @Param via formData string false "视频来源" // @Param coins formData integer false "观看金币" // @Param size formData integer false "文件大小" // @Param mimeType formData string false "影片类型" // @Param sourceURL formData string true "资源url" // @Param sourceID formData string true "上传视频成功后 返回的ID" // @Param filename formData string true "文件名" // @Param resolution formData string false "分辨率" // @Param md5 formData string true "文件摘要" // @Param freeTime formData integer false "免费观影时长" // @Success 200 {string} json "{"msg": "操作成功"}" // @Failure 400 {string} json "{"msg": "操作失败"}" // @Router /vid/spiderSyncSubmit [post] func SpiderSyncSubmit(ctx *gin.Context) { param := vidmod.WebSubmitReq{} if err := ctx.ShouldBind(¶m); err != nil { common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) return } //默认newsType if len(param.NewsType) == 0 { param.NewsType = vidmod.SP } //图集的第一张放到封面中 if (param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC) && len(param.SeriesCover) != 0 { param.Cover = param.SeriesCover[0] param.CoverThumb = param.SeriesCover[0] } code, vid := vidser.SubmitBase(param) if code != stderr.Success { _, _ = vidser.DeleteManyByTTL(vid) common.ServeJSON(ctx, code, vid) return } if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC { common.ServeJSON(ctx, code, vid) return } common.ServeJSON(ctx, code, vid) } // UploadVideoSuccess doc // @Summary 文件管理 - 上传视频成功回调 // @Description 上传视频成功回调 // @Tags WEB-uploaddown // @Accept mpfd,json // @Produce json,html // @Param upload formData file true "文件类型" // @Success 200 {string} json "{"msg": "success" "data":""}" // @Failure 400 {string} json "{"msg": "fail"}" // @Router /web/vid/uploadSuccess [post] func UploadVideoSuccess(c *gin.Context) { var params struct { Id string `json:"id"` } if err := c.ShouldBind(¶ms); err != nil { common.ServeJSON(c, stderr.ErrParamError, err.Error()) return } //通知文件服, 视频文件上传完成 if code := m3u8.UploadSuccess(params.Id); code != 200 { log.Error("UploadVideoSuccess m3u8.UploadSuccess code != 200") common.ServeJSON(c, stderr.Failure, "") return } common.ServeJSON(c, stderr.Success, "") }