deeme commited on
Commit
d20ef33
·
verified ·
1 Parent(s): 27a5591

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +11 -4
  2. cookie.py +94 -0
  3. deps.py +47 -0
  4. main.py +125 -0
  5. requirements.txt +6 -0
  6. schemas.py +53 -0
  7. utils.py +75 -0
Dockerfile CHANGED
@@ -1,5 +1,12 @@
1
- FROM wlhtea/suno2openai:latest
2
 
3
- ENV BASE_URL=https://studio-api.suno.ai
4
- ENV SESSION_ID=cookie
5
- ENV SQL_DK=3306
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim-buster
2
 
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt ./
6
+ RUN --mount=type=cache,target=/root/.cache/pip \
7
+ pip install -r requirements.txt --no-cache-dir
8
+
9
+ COPY . .
10
+
11
+ EXPOSE 8000
12
+ CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000" ]
cookie.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding:utf-8 -*-
2
+
3
+ import os
4
+ import time
5
+ from http.cookies import SimpleCookie
6
+ from threading import Thread
7
+
8
+ import requests
9
+
10
+ from utils import COMMON_HEADERS
11
+
12
+
13
+ class SunoCookie:
14
+ def __init__(self):
15
+ self.cookie = SimpleCookie()
16
+ self.session_id = None
17
+ self.token = None
18
+ self.is_disabled = False
19
+
20
+ def load_cookie(self, cookie_str):
21
+ self.cookie.load(cookie_str)
22
+
23
+ def get_cookie(self):
24
+ return ";".join([f"{i}={self.cookie.get(i).value}" for i in self.cookie.keys()])
25
+
26
+ def set_session_id(self, session_id):
27
+ self.session_id = session_id
28
+
29
+ def get_session_id(self):
30
+ return self.session_id
31
+
32
+ def get_token(self):
33
+ return self.token
34
+
35
+ def set_token(self, token: str):
36
+ self.token = token
37
+
38
+ def disable(self):
39
+ self.is_disabled = True
40
+
41
+ def enable(self):
42
+ self.is_disabled = False
43
+
44
+ def load_env_cookies():
45
+ suno_auths = {}
46
+ for key, value in os.environ.items():
47
+ if key.startswith("SESSION_ID"):
48
+ try:
49
+ i = int(key[10:]) # 提取 SESSION_ID 后的数字
50
+ cookie = os.getenv(f"COOKIE{i}")
51
+ if cookie:
52
+ suno_auth = SunoCookie()
53
+ suno_auth.set_session_id(value)
54
+ suno_auth.load_cookie(cookie)
55
+ suno_auths[i] = suno_auth
56
+ except ValueError:
57
+ continue
58
+ return suno_auths
59
+
60
+ def update_token(suno_cookie: SunoCookie):
61
+ headers = {"cookie": suno_cookie.get_cookie()}
62
+ headers.update(COMMON_HEADERS)
63
+ session_id = suno_cookie.get_session_id()
64
+
65
+ resp = requests.post(
66
+ url=f"https://clerk.suno.com/v1/client/sessions/{session_id}/tokens?_clerk_js_version=5.22.3",
67
+ headers=headers,
68
+ )
69
+
70
+ resp_headers = dict(resp.headers)
71
+ set_cookie = resp_headers.get("Set-Cookie")
72
+ suno_cookie.load_cookie(set_cookie)
73
+ token = resp.json().get("jwt")
74
+ suno_cookie.set_token(token)
75
+ # print(set_cookie)
76
+ # print(f"*** token -> {token} ***")
77
+
78
+
79
+ def keep_alive(suno_cookie: SunoCookie):
80
+ while True:
81
+ try:
82
+ update_token(suno_cookie)
83
+ except Exception as e:
84
+ print(e)
85
+ finally:
86
+ time.sleep(5)
87
+
88
+ def start_keep_alive(suno_auths):
89
+ for suno_auth in suno_auths.values():
90
+ t = Thread(target=keep_alive, args=(suno_auth,))
91
+ t.start()
92
+
93
+ suno_auths = load_env_cookies()
94
+ start_keep_alive(suno_auths)
deps.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding:utf-8 -*-
2
+
3
+ from cookie import suno_auths
4
+ from utils import get_credits
5
+ import logging
6
+ import asyncio
7
+
8
+ async def get_token():
9
+ disabled_accounts = set()
10
+
11
+ while True:
12
+ for i, suno_auth in sorted(suno_auths.items()):
13
+ if suno_auth.is_disabled or i in disabled_accounts:
14
+ continue
15
+
16
+ token = suno_auth.get_token()
17
+
18
+ try:
19
+ credits_info = await get_credits(token)
20
+ credits = credits_info.get('credits_left', 0)
21
+ logging.info(f"当前账号 {suno_auth.get_session_id()} 积分: {credits}")
22
+
23
+ if credits > 5:
24
+ return token
25
+ except Exception as e:
26
+ logging.error(f"账号 {suno_auth.get_session_id()} 获取积分失败: {e}")
27
+ suno_auth.disable()
28
+ disabled_accounts.add(i)
29
+
30
+ if len(disabled_accounts) == len(suno_auths):
31
+ logging.warning("所有账号都已被禁用,等待 1 小时后重试...")
32
+ await asyncio.sleep(3600) # 等待1小时 (3600秒)
33
+ # 重置所有账号的禁用状态
34
+ for suno_auth in suno_auths.values():
35
+ suno_auth.enable()
36
+ disabled_accounts.clear()
37
+ else:
38
+ logging.warning("所有可用账号积分已用尽,等待 1 小时后重试...")
39
+ await asyncio.sleep(3600) # 等待1小时 (3600秒)
40
+
41
+ # 在程序启动时重置所有账号的禁用状态
42
+ def reset_account_status():
43
+ for suno_auth in suno_auths.values():
44
+ suno_auth.enable()
45
+
46
+ # 确保在主程序中调用此函数
47
+ reset_account_status()
main.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding:utf-8 -*-
2
+
3
+ import json
4
+ import logging
5
+ from fastapi import Depends, FastAPI, HTTPException, Request, status
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ import schemas
9
+ from deps import get_token, reset_account_status
10
+ from utils import generate_lyrics, generate_music, get_feed, get_lyrics, get_credits
11
+ from cookie import suno_auths
12
+
13
+ app = FastAPI()
14
+
15
+ app.add_middleware(
16
+ CORSMiddleware,
17
+ allow_origins=["*"],
18
+ allow_credentials=True,
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ # 配置日志
24
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
25
+
26
+ @app.on_event("startup")
27
+ async def startup_event():
28
+ # 在应用启动时重置所有账号状态
29
+ reset_account_status()
30
+ logging.info("所有账号状态已重置")
31
+
32
+ @app.get("/")
33
+ async def get_root():
34
+ return schemas.Response()
35
+
36
+ @app.post("/generate")
37
+ async def generate(
38
+ data: schemas.CustomModeGenerateParam, token: str = Depends(get_token)
39
+ ):
40
+ try:
41
+ resp = await generate_music(data.dict(), token)
42
+ return resp
43
+ except Exception as e:
44
+ logging.error(f"生成音乐失败: {str(e)}")
45
+ raise HTTPException(
46
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
47
+ )
48
+
49
+ @app.post("/generate/description-mode")
50
+ async def generate_with_song_description(
51
+ data: schemas.DescriptionModeGenerateParam, token: str = Depends(get_token)
52
+ ):
53
+ try:
54
+ resp = await generate_music(data.dict(), token)
55
+ return resp
56
+ except Exception as e:
57
+ logging.error(f"根据描述生成音乐失败: {str(e)}")
58
+ raise HTTPException(
59
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
60
+ )
61
+
62
+ @app.get("/feed/{aid}")
63
+ async def fetch_feed(aid: str, token: str = Depends(get_token)):
64
+ try:
65
+ resp = await get_feed(aid, token)
66
+ return resp
67
+ except Exception as e:
68
+ logging.error(f"获取feed失败: {str(e)}")
69
+ raise HTTPException(
70
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
71
+ )
72
+
73
+ @app.post("/generate/lyrics/")
74
+ async def generate_lyrics_post(request: Request, token: str = Depends(get_token)):
75
+ req = await request.json()
76
+ prompt = req.get("prompt")
77
+ if prompt is None:
78
+ raise HTTPException(
79
+ detail="prompt is required", status_code=status.HTTP_400_BAD_REQUEST
80
+ )
81
+
82
+ try:
83
+ resp = await generate_lyrics(prompt, token)
84
+ return resp
85
+ except Exception as e:
86
+ logging.error(f"生成歌词失败: {str(e)}")
87
+ raise HTTPException(
88
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
89
+ )
90
+
91
+ @app.get("/lyrics/{lid}")
92
+ async def fetch_lyrics(lid: str, token: str = Depends(get_token)):
93
+ try:
94
+ resp = await get_lyrics(lid, token)
95
+ return resp
96
+ except Exception as e:
97
+ logging.error(f"获取歌词失败: {str(e)}")
98
+ raise HTTPException(
99
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
100
+ )
101
+
102
+ @app.get("/get_credits")
103
+ async def fetch_credits(token: str = Depends(get_token)):
104
+ try:
105
+ resp = await get_credits(token)
106
+ return resp
107
+ except Exception as e:
108
+ logging.error(f"获取积分失败: {str(e)}")
109
+ raise HTTPException(
110
+ detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
111
+ )
112
+
113
+ @app.get("/account_status")
114
+ async def get_account_status():
115
+ status = {}
116
+ for i, suno_auth in suno_auths.items():
117
+ status[i] = {
118
+ "session_id": suno_auth.get_session_id(),
119
+ "is_disabled": suno_auth.is_disabled
120
+ }
121
+ return status
122
+
123
+ if __name__ == "__main__":
124
+ import uvicorn
125
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ aiohttp
2
+ python-dotenv
3
+ fastapi
4
+ uvicorn
5
+ pydantic
6
+ requests
schemas.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding:utf-8 -*-
2
+
3
+ from datetime import datetime
4
+ from typing import Any, List, Optional, Union
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class Response(BaseModel):
10
+ code: Optional[int] = 0
11
+ msg: Optional[str] = "success"
12
+ data: Optional[Any] = None
13
+
14
+
15
+ class CustomModeGenerateParam(BaseModel):
16
+ """Generate with Custom Mode"""
17
+
18
+ prompt: str = Field(..., description="lyrics")
19
+ mv: str = Field(
20
+ ...,
21
+ description="model version, default: chirp-v3-0",
22
+ examples=["chirp-v3-0"],
23
+ )
24
+ title: str = Field(..., description="song title")
25
+ tags: str = Field(..., description="style of music")
26
+ continue_at: Optional[int] = Field(
27
+ default=None,
28
+ description="continue a new clip from a previous song, format number",
29
+ examples=[120],
30
+ )
31
+ continue_clip_id: Optional[str] = None
32
+
33
+ negative_tags: str = ""
34
+
35
+
36
+ class DescriptionModeGenerateParam(BaseModel):
37
+ """Generate with Song Description"""
38
+
39
+ gpt_description_prompt: str
40
+ make_instrumental: bool = False
41
+ mv: str = Field(
42
+ default='chirp-v3-0',
43
+ description="model version, default: chirp-v3-0",
44
+ examples=["chirp-v3-0"],
45
+ )
46
+
47
+ prompt: str = Field(
48
+ default="",
49
+ description="Placeholder, keep it as an empty string, do not modify it",
50
+ )
51
+
52
+ generation_type: str = "TEXT"
53
+ user_uploaded_images_b64: str = ""
utils.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+
5
+ import aiohttp
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ BASE_URL = "https://studio-api.suno.ai"
11
+
12
+ COMMON_HEADERS = {
13
+ "Content-Type": "text/plain;charset=UTF-8",
14
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
15
+ "Referer": "https://suno.com",
16
+ "Origin": "https://suno.com",
17
+ }
18
+
19
+
20
+ async def fetch(url, headers=None, data=None, method="POST"):
21
+ if headers is None:
22
+ headers = {}
23
+ headers.update(COMMON_HEADERS)
24
+ if data is not None:
25
+ data = json.dumps(data)
26
+
27
+ print(data, method, headers, url)
28
+
29
+ async with aiohttp.ClientSession() as session:
30
+ try:
31
+ async with session.request(
32
+ method=method, url=url, data=data, headers=headers
33
+ ) as resp:
34
+ return await resp.json()
35
+ except Exception as e:
36
+ return f"An error occurred: {e}"
37
+
38
+
39
+ async def get_feed(ids, token):
40
+ headers = {"Authorization": f"Bearer {token}"}
41
+ api_url = f"{BASE_URL}/api/feed/?ids={ids}"
42
+ response = await fetch(api_url, headers, method="GET")
43
+ return response
44
+
45
+
46
+ async def generate_music(data, token):
47
+ headers = {"Authorization": f"Bearer {token}"}
48
+ api_url = f"{BASE_URL}/api/generate/v2/"
49
+ response = await fetch(api_url, headers, data)
50
+ return response
51
+
52
+
53
+ async def generate_lyrics(prompt, token):
54
+ headers = {"Authorization": f"Bearer {token}"}
55
+ api_url = f"{BASE_URL}/api/generate/lyrics/"
56
+ data = {"prompt": prompt}
57
+ return await fetch(api_url, headers, data)
58
+
59
+
60
+ async def get_lyrics(lid, token):
61
+ headers = {"Authorization": f"Bearer {token}"}
62
+ api_url = f"{BASE_URL}/api/generate/lyrics/{lid}"
63
+ return await fetch(api_url, headers, method="GET")
64
+
65
+
66
+ async def get_credits(token):
67
+ headers = {"Authorization": f"Bearer {token}"}
68
+ api_url = f"{BASE_URL}/api/billing/info/"
69
+ respose = await fetch(api_url, headers, method="GET")
70
+ return {
71
+ "credits_left": respose['total_credits_left'],
72
+ #"period": respose['period'],
73
+ "monthly_limit": respose['monthly_limit'],
74
+ "monthly_usage": respose['monthly_usage']
75
+ }