first commit
This commit is contained in:
parent
8e58e24d2e
commit
973c47ff25
16 changed files with 866 additions and 0 deletions
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
__pycache__/
|
||||||
|
images/
|
||||||
|
|
||||||
|
.env
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Build and Release Folders
|
||||||
|
bin-debug/
|
||||||
|
bin-release/
|
||||||
|
[Oo]bj/
|
||||||
|
[Bb]in/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Other files and folders
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.swf
|
||||||
|
*.air
|
||||||
|
*.ipa
|
||||||
|
*.apk
|
||||||
|
|
||||||
|
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
|
||||||
|
# should NOT be excluded as they contain compiler settings and other important
|
||||||
|
# information for Eclipse / Flash Builder.
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM python:3.10-slim-buster
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8003" ]
|
201
LICENSE
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
9
README.md
Normal file
9
README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Suno-api
|
||||||
|
Fork from [Suno-API](https://github.com/SunoAI-API/Suno-API)
|
||||||
|
# Features
|
||||||
|
Compared with the original version, it has the following features
|
||||||
|
- ALL features of [Suno-API](https://github.com/SunoAI-API/Suno-API)
|
||||||
|
- Added subscription interface
|
||||||
|
- Support multi-user
|
||||||
|
- Change config file to yaml
|
||||||
|
- Support dynamic loading configuration
|
24
accounts.py
Normal file
24
accounts.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from logger import logger
|
||||||
|
from typing import Optional,List,Dict,Any
|
||||||
|
from config import Settings
|
||||||
|
|
||||||
|
def accounts_info(config: Settings) -> Optional[Dict[str, Any]]:
|
||||||
|
if config is None:
|
||||||
|
logger.error({"config_err": "config is null, can not get accounts"})
|
||||||
|
return None
|
||||||
|
return config.accounts
|
||||||
|
|
||||||
|
def accounts_list(config: Settings) -> Optional[List[str]]:
|
||||||
|
info = accounts_info(config)
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
logger.error({"config_account_err": "can not get accounts from config file"})
|
||||||
|
return None
|
||||||
|
|
||||||
|
accounts=[]
|
||||||
|
for account_id,value in info.items():
|
||||||
|
if not value.get("session_id") or not value.get("cookie"):
|
||||||
|
logger.error({"config_account_err": "account config error"})
|
||||||
|
return {"config_account_err": "account config error"}
|
||||||
|
accounts.append(account_id)
|
||||||
|
return accounts
|
38
config.py
Normal file
38
config.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import yaml
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
# Configuration path
|
||||||
|
config_path = "config/config.yaml"
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "allow"
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, path: str = config_path):
|
||||||
|
self.path = path
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
try:
|
||||||
|
with open(self.path, 'r') as file:
|
||||||
|
logger.info({"config_load": f"load_config:{self.path}"})
|
||||||
|
data = yaml.safe_load(file)
|
||||||
|
logger.info({"config_load": f"config load successfully"})
|
||||||
|
self.config = Settings(**data)
|
||||||
|
#logger.info({"config_update": f"Config updated with: {data}"})
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.error({"config_err": f"File not found: {e}"})
|
||||||
|
raise Exception(f"File not found: {e}")
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
logger.error({"config_err": f"YAML error: {e}"})
|
||||||
|
raise Exception(f"YAML error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"config_err": f"Unexpected error: {e}"})
|
||||||
|
raise Exception(f"Failed to load config: {e}")
|
||||||
|
|
||||||
|
def reload_config(self) -> None:
|
||||||
|
self.load_config()
|
14
config/config.yaml
Normal file
14
config/config.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
base_url: "https://studio-api.suno.ai"
|
||||||
|
|
||||||
|
subscribe:
|
||||||
|
500: a79f3640-d366-4c21-8d85-b35dad751f70
|
||||||
|
4000: 4cb5c6d9-bdb2-4bc1-9f62-c2cc55d48586
|
||||||
|
|
||||||
|
accounts:
|
||||||
|
account_1:
|
||||||
|
session_id: xxx
|
||||||
|
cookie: xxx
|
||||||
|
|
||||||
|
account_2:
|
||||||
|
session_id: xxx
|
||||||
|
cookie: xxx
|
121
cookie.py
Normal file
121
cookie.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
import time
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from threading import Thread, Event
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from utils import COMMON_HEADERS, logger
|
||||||
|
from accounts import accounts_info
|
||||||
|
|
||||||
|
class SunoCookie:
|
||||||
|
def __init__(self, account_id=None, session_id=None, cookie_str=None):
|
||||||
|
self.account_id = account_id
|
||||||
|
self.cookie = SimpleCookie()
|
||||||
|
self.session_id = session_id
|
||||||
|
self.token = None
|
||||||
|
self.last_called = datetime.now()
|
||||||
|
self.status = True
|
||||||
|
|
||||||
|
if cookie_str:
|
||||||
|
self.load_cookie(cookie_str)
|
||||||
|
|
||||||
|
def load_cookie(self, cookie_str):
|
||||||
|
self.cookie.load(cookie_str)
|
||||||
|
|
||||||
|
def get_cookie(self):
|
||||||
|
return ";".join([f"{i}={self.cookie.get(i).value}" for i in self.cookie.keys()])
|
||||||
|
|
||||||
|
def set_session_id(self, session_id):
|
||||||
|
self.session_id = session_id
|
||||||
|
|
||||||
|
def get_session_id(self):
|
||||||
|
return self.session_id
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def set_token(self, token: str):
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
def set_status(self, status: bool):
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def update_last_called(self):
|
||||||
|
logger.info({"update_last_account_id": self.account_id})
|
||||||
|
self.last_called = datetime.now()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (f"SunoCookie(account_id={self.account_id},"
|
||||||
|
f"last_called={self.last_called},"
|
||||||
|
f"status={self.status})"
|
||||||
|
)
|
||||||
|
|
||||||
|
class LoadAccounts():
|
||||||
|
def __init__(self):
|
||||||
|
self.accounts = []
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.accounts)
|
||||||
|
|
||||||
|
def set_accounts(self,info: dict) -> None:
|
||||||
|
for account_id,details in info.items():
|
||||||
|
session_id = details["session_id"]
|
||||||
|
cookie_str = details["cookie"]
|
||||||
|
self.accounts.append(SunoCookie(account_id,session_id,cookie_str))
|
||||||
|
|
||||||
|
def get_accounts(self):
|
||||||
|
return self.accounts
|
||||||
|
|
||||||
|
def reset_accounts(self,info: dict) -> None:
|
||||||
|
self.accounts.clear()
|
||||||
|
self.set_accounts(info)
|
||||||
|
|
||||||
|
|
||||||
|
def update_token(suno_cookie: SunoCookie):
|
||||||
|
headers = {"cookie": suno_cookie.get_cookie()}
|
||||||
|
headers.update(COMMON_HEADERS)
|
||||||
|
session_id = suno_cookie.get_session_id()
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
url=f"https://clerk.suno.com/v1/client/sessions/{session_id}/tokens?_clerk_js_version=5.16.1",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp_headers = dict(resp.headers)
|
||||||
|
set_cookie = resp_headers.get("Set-Cookie")
|
||||||
|
suno_cookie.load_cookie(set_cookie)
|
||||||
|
token = resp.json().get("jwt")
|
||||||
|
suno_cookie.set_token(token)
|
||||||
|
# print(set_cookie)
|
||||||
|
# print(f"*** token -> {token} ***")
|
||||||
|
|
||||||
|
def keep_alive(suno_cookie: SunoCookie, event: Event):
|
||||||
|
while not event.wait(timeout=5):
|
||||||
|
try:
|
||||||
|
update_token(suno_cookie)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
if event.is_set():
|
||||||
|
logger.info({"update_token_info": "preparing to reload thread."})
|
||||||
|
break
|
||||||
|
|
||||||
|
def start_and_manage_threads(suno_cookies, event: Event):
|
||||||
|
threads = []
|
||||||
|
for suno_cookie in suno_cookies:
|
||||||
|
t = Thread(target=keep_alive, args=(suno_cookie, event))
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
return threads
|
||||||
|
|
||||||
|
def reload_threads(suno_cookies, threads: list, event: Event):
|
||||||
|
event.set()
|
||||||
|
if threads:
|
||||||
|
for t in threads:
|
||||||
|
if t.is_alive():
|
||||||
|
t.join()
|
||||||
|
event.clear()
|
||||||
|
return start_and_manage_threads(suno_cookies, event)
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
suno-api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8003:8003"
|
||||||
|
volumes:
|
||||||
|
- ./config:/app/config
|
||||||
|
logging:
|
||||||
|
options:
|
||||||
|
tag: "{{.Name}}"
|
6
flush.py
Normal file
6
flush.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||||
|
from cookie import update_token,suno_auth
|
||||||
|
|
||||||
|
schedulers = BlockingScheduler()
|
||||||
|
schedulers.add_job(update_token, "interval", minutes=30,args=[suno_auth])
|
||||||
|
schedulers.start()
|
4
logger.py
Normal file
4
logger.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
253
main.py
Normal file
253
main.py
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
import schemas
|
||||||
|
from config import Config
|
||||||
|
from utils import logger, generate_lyrics, generate_music, get_feed, get_lyrics, get_credits, recharge, get_expire
|
||||||
|
from cookie import LoadAccounts,reload_threads
|
||||||
|
from accounts import accounts_info,accounts_list
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
config_loader = Config()
|
||||||
|
config_loader.load_config()
|
||||||
|
config = config_loader.config
|
||||||
|
|
||||||
|
suno_auth = LoadAccounts()
|
||||||
|
suno_auth.set_accounts(accounts_info(config))
|
||||||
|
|
||||||
|
thread_event = Event()
|
||||||
|
|
||||||
|
threads = reload_threads(suno_auth,None,thread_event)
|
||||||
|
|
||||||
|
# Function to get the least recently used account
|
||||||
|
def get_least_recently_used_account():
|
||||||
|
logger.info({"suno_auth": suno_auth})
|
||||||
|
available_accounts = [account for account in suno_auth if account.get_status()]
|
||||||
|
if available_accounts == []:
|
||||||
|
logger.error({"accounts_err": "no account available"})
|
||||||
|
return None
|
||||||
|
return min(available_accounts, key=lambda x: x.last_called)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_token(account_id: str):
|
||||||
|
for auth in suno_auth:
|
||||||
|
if auth.account_id == account_id:
|
||||||
|
return auth.get_token()
|
||||||
|
logger.error({"account_err": "Account not found"})
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get_root():
|
||||||
|
return schemas.Response()
|
||||||
|
|
||||||
|
@app.get("/reload")
|
||||||
|
async def reload_configuration():
|
||||||
|
global threads, config_loader,config
|
||||||
|
try:
|
||||||
|
config_loader.reload_config()
|
||||||
|
config = config_loader.config
|
||||||
|
logger.info({"config_reload": "config reload successfully"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"config_reload_err": f"config reload faild: {e}"})
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
try:
|
||||||
|
suno_auth.reset_accounts(accounts_info(config))
|
||||||
|
logger.info({"reset_accounts": "Accounts reset successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"reset_accounts_err": f"reset accounts faild: {e}"})
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
try:
|
||||||
|
threads = reload_threads(suno_auth, threads, thread_event)
|
||||||
|
logger.info({"reload_threads": "Threads reloaded successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"reload_threads_for_update_token_err": f"reload threads faild: {e}"})
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
logger.info({"reload_config_info": "config reload successfully"})
|
||||||
|
return {"config_reload": "reload config successfully"}
|
||||||
|
|
||||||
|
# function for available accounts
|
||||||
|
async def get_accounts_info():
|
||||||
|
for suno_cookie in suno_auth:
|
||||||
|
account_id = suno_cookie.account_id
|
||||||
|
try:
|
||||||
|
resp = await fetch_credits(get_token(account_id))
|
||||||
|
if resp["credits_left"] < 10:
|
||||||
|
suno_cookie.set_status(False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"get_credits_err": f"{account_id} token invalid, {e}"})
|
||||||
|
suno_cookie.set_status(False)
|
||||||
|
|
||||||
|
@app.post("/generate")
|
||||||
|
async def generate(
|
||||||
|
data: schemas.CustomModeGenerateParam
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await get_accounts_info()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"get_available_accounts_g_err": f"faild to update accounts status, {e}"})
|
||||||
|
|
||||||
|
suno_cookie = get_least_recently_used_account()
|
||||||
|
if not suno_cookie:
|
||||||
|
logger.error({"generate_err": "no account available"})
|
||||||
|
return
|
||||||
|
token = suno_cookie.get_token()
|
||||||
|
suno_cookie.update_last_called()
|
||||||
|
logger.info({"account_id": suno_cookie.account_id,"request_data": data})
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await generate_music(config.base_url, data.model_dump(), token)
|
||||||
|
return {"account_id": suno_cookie.account_id, **resp}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/generate/description-mode")
|
||||||
|
async def generate_with_song_description(
|
||||||
|
data: schemas.DescriptionModeGenerateParam
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await get_accounts_info()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"get_available_accounts_d_err": f"faild to update accounts status, {e}"})
|
||||||
|
|
||||||
|
suno_cookie = get_least_recently_used_account()
|
||||||
|
if not suno_cookie:
|
||||||
|
logger.error({"generate_d_err": "no account available"})
|
||||||
|
return
|
||||||
|
token = suno_cookie.get_token()
|
||||||
|
suno_cookie.update_last_called()
|
||||||
|
logger.info({"account_id": suno_cookie.account_id,"request_data": data})
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await generate_music(config.base_url,data.model_dump(), token)
|
||||||
|
return {"account_id": suno_cookie.account_id, **resp}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/feed/{account_id}/{aid}")
|
||||||
|
async def fetch_feed(account_id: str, aid: str, token: str = Depends(get_token)):
|
||||||
|
try:
|
||||||
|
resp = await get_feed(config.base_url, aid, token)
|
||||||
|
logger.info({"feed_resp": resp})
|
||||||
|
return resp
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"feed_err": e})
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/generate/lyrics/")
|
||||||
|
async def generate_lyrics_post(request: Request):
|
||||||
|
try:
|
||||||
|
await get_accounts_info()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"get_available_accounts_l_err": f"faild to update accounts status, {e}"})
|
||||||
|
|
||||||
|
suno_cookie = get_least_recently_used_account()
|
||||||
|
if not suno_cookie:
|
||||||
|
logger.error({"generate_lyrics_err": "no account available"})
|
||||||
|
return
|
||||||
|
token = suno_cookie.get_token()
|
||||||
|
suno_cookie.update_last_called()
|
||||||
|
logger.info(f"request by *** accountid -> {suno_cookie.account_id} ***")
|
||||||
|
|
||||||
|
req = await request.json()
|
||||||
|
prompt = req.get("prompt")
|
||||||
|
if prompt is None:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="prompt is required", status_code=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await generate_lyrics(config.base_url, prompt, token)
|
||||||
|
return {"account_id": suno_cookie.account_id, **resp}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/lyrics/{account_id}/{lid}")
|
||||||
|
async def fetch_lyrics(account_id: str, lid: str, token: str = Depends(get_token)):
|
||||||
|
try:
|
||||||
|
resp = await get_lyrics(config.base_url, lid, token)
|
||||||
|
return resp
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/get_credits/{account_id}")
|
||||||
|
async def fetch_credits(token: str = Depends(get_token)):
|
||||||
|
try:
|
||||||
|
resp = await get_credits(config.base_url, token)
|
||||||
|
logger.info({"credits_resp": resp})
|
||||||
|
return resp
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
# expample:
|
||||||
|
# post: http://localhost:8000/recharge/account_id?auto_subscribe=500
|
||||||
|
@app.post("/recharge/{account_id}")
|
||||||
|
async def recharge_account(account_id: str, auto_subscribe: Optional[str] = None, token: str = Depends(get_token)):
|
||||||
|
sub = config.subscribe
|
||||||
|
data = {
|
||||||
|
"amount": int(auto_subscribe),
|
||||||
|
"id": sub[auto_subscribe]
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
resp = await recharge(config.base_url, data, token)
|
||||||
|
logger.info({f"recharge_{account_id}": "recharge successful"})
|
||||||
|
return resp
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"err_recharge": f"Internal server error: {e}"})
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/recharge/account_all")
|
||||||
|
async def recharge_account(auto_subscribe: Optional[str] = None, token: str = Depends(get_token)):
|
||||||
|
for account_id in accounts_list(config):
|
||||||
|
return await recharge_account(account_id, auto_subscribe, token)
|
||||||
|
|
||||||
|
@app.get("/account/list")
|
||||||
|
async def account_list():
|
||||||
|
try:
|
||||||
|
resp = accounts_list(config)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"err_get_account": f"Internal server error: {e}"})
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/get_expire/{account_id}")
|
||||||
|
async def expire_time(account_id: str):
|
||||||
|
info = accounts_info(config)
|
||||||
|
cookie = info[account_id]["cookie"]
|
||||||
|
try:
|
||||||
|
resp = await get_expire(cookie)
|
||||||
|
|
||||||
|
return resp['response']['sessions'][0]['expire_at']
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({"err_get_expire": f"Internal server error: {e}"})
|
||||||
|
raise HTTPException(
|
||||||
|
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
aiohttp
|
||||||
|
python-dotenv
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
pydantic
|
||||||
|
pydantic-settings
|
||||||
|
requests
|
||||||
|
pyyaml
|
48
schemas.py
Normal file
48
schemas.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Response(BaseModel):
|
||||||
|
code: Optional[int] = 0
|
||||||
|
msg: Optional[str] = "success"
|
||||||
|
data: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CustomModeGenerateParam(BaseModel):
|
||||||
|
"""Generate with Custom Mode"""
|
||||||
|
|
||||||
|
prompt: str = Field(..., description="lyrics")
|
||||||
|
mv: str = Field(
|
||||||
|
...,
|
||||||
|
description="model version, default: chirp-v3-0",
|
||||||
|
examples=["chirp-v3-0"],
|
||||||
|
)
|
||||||
|
title: str = Field(..., description="song title")
|
||||||
|
tags: str = Field(..., description="style of music")
|
||||||
|
continue_at: Optional[int] = Field(
|
||||||
|
default=None,
|
||||||
|
description="continue a new clip from a previous song, format number",
|
||||||
|
examples=[120],
|
||||||
|
)
|
||||||
|
continue_clip_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptionModeGenerateParam(BaseModel):
|
||||||
|
"""Generate with Song Description"""
|
||||||
|
|
||||||
|
gpt_description_prompt: str
|
||||||
|
make_instrumental: bool = False
|
||||||
|
mv: str = Field(
|
||||||
|
default='chirp-v3-0',
|
||||||
|
description="model version, default: chirp-v3-0",
|
||||||
|
examples=["chirp-v3-0"],
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Placeholder, keep it as an empty string, do not modify it",
|
||||||
|
)
|
90
utils.py
Normal file
90
utils.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import json
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
COMMON_HEADERS = {
|
||||||
|
"Content-Type": "text/plain;charset=UTF-8",
|
||||||
|
"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",
|
||||||
|
"Referer": "https://suno.com",
|
||||||
|
"Origin": "https://suno.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fetch(url, headers=None, data=None, method="POST"):
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
headers.update(COMMON_HEADERS)
|
||||||
|
if data is not None:
|
||||||
|
data = json.dumps(data)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.request(
|
||||||
|
method=method, url=url, data=data, headers=headers
|
||||||
|
) as resp:
|
||||||
|
content_type = resp.headers.get("Content-Type","")
|
||||||
|
logger.info(f"Request to {url} with headers {resp.headers} method {method} and data {data}")
|
||||||
|
if "application/json" in content_type:
|
||||||
|
return await resp.json()
|
||||||
|
else:
|
||||||
|
text_response = await resp.text()
|
||||||
|
logger.error(f"Unexpected response format: {text_response}")
|
||||||
|
return f"Unexpected response format: {text_response}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error occurred: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_feed(url, ids, token):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/feed/?ids={ids}"
|
||||||
|
response = await fetch(api_url, headers, method="GET")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_music(url,data, token):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/generate/v2/"
|
||||||
|
response = await fetch(api_url, headers, data)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_lyrics(url,prompt, token):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/generate/lyrics/"
|
||||||
|
data = {"prompt": prompt}
|
||||||
|
return await fetch(api_url, headers, data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lyrics(url,lid, token):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/generate/lyrics/{lid}"
|
||||||
|
return await fetch(api_url, headers, method="GET")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_credits(url,token):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/billing/info/"
|
||||||
|
respose = await fetch(api_url, headers, method="GET")
|
||||||
|
return {
|
||||||
|
"credits_left": respose['total_credits_left'],
|
||||||
|
"period": respose['period'],
|
||||||
|
"monthly_limit": respose['monthly_limit'],
|
||||||
|
"monthly_usage": respose['monthly_usage']
|
||||||
|
}
|
||||||
|
|
||||||
|
async def recharge(url,data, token):
|
||||||
|
if not token or not isinstance(data,dict):
|
||||||
|
logger.error("Invalid token or data")
|
||||||
|
raise
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
api_url = f"{url}/api/billing/purchase-credits/"
|
||||||
|
return await fetch(api_url, headers, data)
|
||||||
|
|
||||||
|
async def get_expire(cookie):
|
||||||
|
if not cookie:
|
||||||
|
logger.error("Invalid cookie at get expire")
|
||||||
|
raise
|
||||||
|
headers = {"Cookie": cookie}
|
||||||
|
api_url = "https://clerk.suno.com/v1/client?_clerk_js_version=5.16.1"
|
||||||
|
return await fetch(api_url, headers, method="GET")
|
Loading…
Add table
Add a link
Reference in a new issue