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 @@