diff --git a/api/core/types/task.go b/api/core/types/task.go index 72b0d7c6..2affae7d 100644 --- a/api/core/types/task.go +++ b/api/core/types/task.go @@ -87,11 +87,13 @@ type SunoTask struct { Type int `json:"type"` TaskId string `json:"task_id"` Title string `json:"title"` - RefTaskId string `json:"ref_task_id"` - RefSongId string `json:"ref_song_id"` + RefTaskId string `json:"ref_task_id,omitempty"` + RefSongId string `json:"ref_song_id,omitempty"` Prompt string `json:"prompt"` // 提示词/歌词 Tags string `json:"tags"` Model string `json:"model"` - Instrumental bool `json:"instrumental"` // 是否纯音乐 - ExtendSecs int `json:"extend_secs"` // 延长秒杀 + Instrumental bool `json:"instrumental"` // 是否纯音乐 + ExtendSecs int `json:"extend_secs,omitempty"` // 延长秒杀 + SongId string `json:"song_id,omitempty"` // 合并歌曲ID + AudioURL string `json:"audio_url"` // 用户上传音频地址 } diff --git a/api/handler/invite_handler.go b/api/handler/invite_handler.go index 3e4fdb53..e6e5c029 100644 --- a/api/handler/invite_handler.go +++ b/api/handler/invite_handler.go @@ -9,7 +9,6 @@ package handler import ( "geekai/core" - "geekai/core/types" "geekai/store/model" "geekai/store/vo" "geekai/utils" @@ -59,23 +58,16 @@ func (h *InviteHandler) Code(c *gin.Context) { // List Log 用户邀请记录 func (h *InviteHandler) List(c *gin.Context) { - - var data struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } + page := h.GetInt(c, "page", 1) + pageSize := h.GetInt(c, "page_size", 20) userId := h.GetLoginUserId(c) session := h.DB.Session(&gorm.Session{}).Where("inviter_id = ?", userId) var total int64 session.Model(&model.InviteLog{}).Count(&total) var items []model.InviteLog var list = make([]vo.InviteLog, 0) - offset := (data.Page - 1) * data.PageSize - res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items) + offset := (page - 1) * pageSize + res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items) if res.Error == nil { for _, item := range items { var v vo.InviteLog @@ -89,7 +81,7 @@ func (h *InviteHandler) List(c *gin.Context) { } } } - resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list)) + resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list)) } // Hits 访问邀请码 diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go index 2ceed2c1..9151a0eb 100644 --- a/api/handler/suno_handler.go +++ b/api/handler/suno_handler.go @@ -72,15 +72,32 @@ func (h *SunoHandler) Create(c *gin.Context) { Tags string `json:"tags"` Title string `json:"title"` Type int `json:"type"` - RefTaskId string `json:"ref_task_id"` // 续写的任务id - ExtendSecs int `json:"extend_secs"` // 续写秒数 - RefSongId string `json:"ref_song_id"` // 续写的歌曲id + RefTaskId string `json:"ref_task_id"` // 续写的任务id + ExtendSecs int `json:"extend_secs"` // 续写秒数 + RefSongId string `json:"ref_song_id"` // 续写的歌曲id + SongId string `json:"song_id,omitempty"` // 要拼接的歌曲id + AudioURL string `json:"audio_url,omitempty"` // 上传自己创作的歌曲 } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } + // 歌曲拼接 + if data.SongId != "" && data.Type == 3 { + var song model.SunoJob + if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil { + data.Instrumental = song.Instrumental + data.Model = song.ModelName + data.Tags = song.Tags + } + // 拼接歌词 + var refSong model.SunoJob + if err := h.DB.Where("song_id = ?", data.RefSongId).First(&refSong).Error; err == nil { + data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt) + } + } + // 插入数据库 job := model.SunoJob{ UserId: int(h.GetLoginUserId(c)), @@ -118,6 +135,8 @@ func (h *SunoHandler) Create(c *gin.Context) { Tags: data.Tags, Model: data.Model, Instrumental: data.Instrumental, + SongId: data.SongId, + AudioURL: data.AudioURL, }) // update user's power diff --git a/api/main.go b/api/main.go index 2cf9a407..70611a6b 100644 --- a/api/main.go +++ b/api/main.go @@ -400,7 +400,7 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) { group := s.Engine.Group("/api/invite/") group.GET("code", h.Code) - group.POST("list", h.List) + group.GET("list", h.List) group.GET("hits", h.Hits) }), diff --git a/api/service/suno/service.go b/api/service/suno/service.go index b9599303..a49bcb47 100644 --- a/api/service/suno/service.go +++ b/api/service/suno/service.go @@ -82,14 +82,21 @@ func (s *Service) Run() { logger.Errorf("taking task with error: %v", err) continue } - - r, err := s.Create(task) + var r RespVo + if task.Type == 3 && task.SongId != "" { // 歌曲拼接 + r, err = s.Merge(task) + } else if task.Type == 4 && task.AudioURL != "" { // 上传歌曲 + r, err = s.Upload(task) + } else { // 歌曲创作 + r, err = s.Create(task) + } if err != nil { logger.Errorf("create task with error: %v", err) s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ "err_msg": err.Error(), "progress": service.FailTaskProgress, }) + s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed}) continue } @@ -138,7 +145,94 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) { } var res RespVo - apiURL := fmt.Sprintf("%s/task/suno/v1/submit/music", apiKey.ApiURL) + apiURL := fmt.Sprintf("%s/suno/submit/music", apiKey.ApiURL) + logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody) + r, err := req.C().R(). + SetHeader("Authorization", "Bearer "+apiKey.Value). + SetBody(reqBody). + Post(apiURL) + if err != nil { + return RespVo{}, fmt.Errorf("请求 API 出错:%v", err) + } + + body, _ := io.ReadAll(r.Body) + err = json.Unmarshal(body, &res) + if err != nil { + return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + if res.Code != "success" { + return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message) + } + // update the last_use_at for api key + apiKey.LastUsedAt = time.Now().Unix() + session.Updates(&apiKey) + res.Channel = apiKey.ApiURL + return res, nil +} + +func (s *Service) Merge(task types.SunoTask) (RespVo, error) { + // 读取 API KEY + var apiKey model.ApiKey + session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true) + if task.Channel != "" { + session = session.Where("api_url", task.Channel) + } + tx := session.Order("last_used_at DESC").First(&apiKey) + if tx.Error != nil { + return RespVo{}, errors.New("no available API KEY for Suno") + } + + reqBody := map[string]interface{}{ + "clip_id": task.SongId, + "is_infill": false, + } + + var res RespVo + apiURL := fmt.Sprintf("%s/suno/submit/concat", apiKey.ApiURL) + logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody) + r, err := req.C().R(). + SetHeader("Authorization", "Bearer "+apiKey.Value). + SetBody(reqBody). + Post(apiURL) + if err != nil { + return RespVo{}, fmt.Errorf("请求 API 出错:%v", err) + } + + body, _ := io.ReadAll(r.Body) + err = json.Unmarshal(body, &res) + if err != nil { + return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + if res.Code != "success" { + return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message) + } + // update the last_use_at for api key + apiKey.LastUsedAt = time.Now().Unix() + session.Updates(&apiKey) + res.Channel = apiKey.ApiURL + return res, nil +} + +func (s *Service) Upload(task types.SunoTask) (RespVo, error) { + // 读取 API KEY + var apiKey model.ApiKey + session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true) + if task.Channel != "" { + session = session.Where("api_url", task.Channel) + } + tx := session.Order("last_used_at DESC").First(&apiKey) + if tx.Error != nil { + return RespVo{}, errors.New("no available API KEY for Suno") + } + + reqBody := map[string]interface{}{ + "url": task.AudioURL, + } + + var res RespVo + apiURL := fmt.Sprintf("%s/suno/uploads/audio-url", apiKey.ApiURL) logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody) r, err := req.C().R(). SetHeader("Authorization", "Bearer "+apiKey.Value). @@ -339,7 +433,7 @@ func (s *Service) QueryTask(taskId string, channel string) (QueryRespVo, error) return QueryRespVo{}, errors.New("no available API KEY for Suno") } - apiURL := fmt.Sprintf("%s/task/suno/v1/fetch/%s", apiKey.ApiURL, taskId) + apiURL := fmt.Sprintf("%s/suno/fetch/%s", apiKey.ApiURL, taskId) var res QueryRespVo r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL) diff --git a/api/store/vo/suno_job.go b/api/store/vo/suno_job.go index fbc752de..97a18a3a 100644 --- a/api/store/vo/suno_job.go +++ b/api/store/vo/suno_job.go @@ -5,7 +5,7 @@ type SunoJob struct { UserId int `json:"user_id"` Channel string `json:"channel"` Title string `json:"title"` - Type string `json:"type"` + Type int `json:"type"` TaskId string `json:"task_id"` RefTaskId string `json:"ref_task_id"` // 续写的任务id Tags string `json:"tags"` // 歌曲风格和标签 diff --git a/web/src/assets/css/suno.styl b/web/src/assets/css/suno.styl index ffe39119..a193d7de 100644 --- a/web/src/assets/css/suno.styl +++ b/web/src/assets/css/suno.styl @@ -13,6 +13,13 @@ display flex flex-flow row justify-content: space-between; + + .upload-music { + .iconfont { + margin-right 5px + font-size 14px + } + } } .params { @@ -85,6 +92,10 @@ height 50px border-radius 10px } + .icon-mp3 { + font-size 42px + color #A85295 + } .title { display flex margin-left 10px @@ -266,7 +277,7 @@ } .right { - min-width 320px; + min-width 350px; font-size 14px padding 0 15px diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index 586dc701..254a31ff 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1723593727785') format('woff2'), - url('iconfont.woff?t=1723593727785') format('woff'), - url('iconfont.ttf?t=1723593727785') format('truetype'); + src: url('iconfont.woff2?t=1725000514997') format('woff2'), + url('iconfont.woff?t=1725000514997') format('woff'), + url('iconfont.ttf?t=1725000514997') format('truetype'); } .iconfont { @@ -13,6 +13,18 @@ -moz-osx-font-smoothing: grayscale; } +.icon-merge:before { + content: "\e901"; +} + +.icon-upload:before { + content: "\e611"; +} + +.icon-concat:before { + content: "\e630"; +} + .icon-email:before { content: "\e670"; } @@ -77,7 +89,7 @@ content: "\e608"; } -.icon-mp:before { +.icon-mp3:before { content: "\e6c4"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index 886819cf..a106bda2 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}})(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index 0d177ae5..aa4ed4ef 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -1,10 +1,31 @@ { "id": "4125778", - "name": "chatgpt", + "name": "geekai", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "8094809", + "name": "merge-cells", + "font_class": "merge", + "unicode": "e901", + "unicode_decimal": 59649 + }, + { + "icon_id": "10278208", + "name": "上传", + "font_class": "upload", + "unicode": "e611", + "unicode_decimal": 58897 + }, + { + "icon_id": "23538484", + "name": "拼接", + "font_class": "concat", + "unicode": "e630", + "unicode_decimal": 58928 + }, { "icon_id": "15838472", "name": "email", @@ -120,7 +141,7 @@ { "icon_id": "4318807", "name": "mp3", - "font_class": "mp", + "font_class": "mp3", "unicode": "e6c4", "unicode_decimal": 59076 }, diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 6e879bd8..92b779cd 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index ef68b85f..7e1fb13e 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index f33f132c..0d306dab 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/components/InviteList.vue b/web/src/components/InviteList.vue index 91567e79..6fe482ed 100644 --- a/web/src/components/InviteList.vue +++ b/web/src/components/InviteList.vue @@ -33,11 +33,10 @@