初始提交

This commit is contained in:
2024-05-26 13:25:28 +08:00
commit 07e5009a4f
19 changed files with 1669 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
+15
View File
@@ -0,0 +1,15 @@
FROM alpine:3.18
MAINTAINER CattySteve cattysteve89265@163.com
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add python3 py3-pip gcc g++ python3-dev curl uuidgen
RUN echo flag{$(uuidgen)} > /flag
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
COPY . /opt/galaxy
WORKDIR /opt/galaxy
RUN pip install -r ./requirements_deployment.txt
EXPOSE 5001
CMD cd /opt/galaxy && python main.py
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s \
CMD curl http://localhost:5001 > /dev/null
+62
View File
@@ -0,0 +1,62 @@
try:
from Provider import SessionProvider, SongStatus, AccountType
except:
from Extensions.Provider import SessionProvider, SongStatus, AccountType
import httpx
from functools import lru_cache
import re
from enum import Enum
from bilibili_api import user, login_func, sync, login, Credential
from pyzbar import pyzbar
from PIL import Image
import qrcode
import time
class BilibiliSession(SessionProvider):
sessionKey = None
session = None
providerName = "Bilibili"
URLKeyword = ["b23.tv", "bilibili.com", "bilibili.tv"]
accountType = AccountType.ACCOUNT_NOLOGIN
def login(self) -> dict:
status, self.session = login_func.check_qrcode_events(self.sessionKey)
print(status)
if status == login_func.QrCodeLoginEvents.SCAN:
return {"status": "400", "msg": "请扫描二维码"}, 400
elif status == login_func.QrCodeLoginEvents.CONF:
return {"status": "400", "msg": "请在手机上确认登录"}, 400
elif status == login_func.QrCodeLoginEvents.TIMEOUT:
return {"status": "400", "msg": "二维码已过期,请重新获取"}, 400
elif status == login_func.QrCodeLoginEvents.DONE:
self.status.cache_clear()
print("success!")
return {"status": "200", "msg": "登录成功"}, 200
else:
return {"status": "400", "msg": "未知错误"}, 400
@lru_cache(maxsize=8)
def status(self) -> dict:
return sync(user.get_self_info(self.session))
def userStatus(self) -> dict:
return self.status()
def logout(self) -> dict:
return {"status": 201, "msg": "无法登出,直接换帐号就好了"}, 201
def qrcode(self):
_session = login.update_qrcode_data()
loginURL = _session["url"]
self.sessionKey = _session["qrcode_key"]
qrcode.make(loginURL).save("/tmp/qrcode.png")
return loginURL
if __name__ == "__main__":
bisess = BilibiliSession()
print(bisess.qrcode())
while(True):
if bisess.login()[1] == 200:
break
time.sleep(2)
print(bisess.userStatus())
+160
View File
@@ -0,0 +1,160 @@
from pyncm import apis as ncm
import pyncm
try:
from Provider import SessionProvider, SongStatus, AccountType
except:
from Extensions.Provider import SessionProvider, SongStatus, AccountType
import httpx
from functools import lru_cache
import re
from enum import Enum
class NeteaseSession(SessionProvider):
session = None
providerName = "Netease"
URLKeyword = ["music.163.com", "163cn.tv", "网易云", "netease"]
accountType = AccountType.ACCOUNT_NOLOGIN
def updateAccountType(self) -> AccountType:
response = ncm.login.GetCurrentLoginStatus()["account"]
if not response:
self.accountType = AccountType.ACCOUNT_NOLOGIN
return
if response["paidFee"]:
self.accountType = AccountType.ACCOUNT_VIP
else:
self.accountType = AccountType.ACCOUNT_USER
def login(self) -> dict:
self.status.cache_clear()
status = ncm.login.LoginQrcodeCheck(self.session)
if status["code"] == 803:
ncm.login.WriteLoginInfo(ncm.login.GetCurrentLoginStatus())
self.updateAccountType()
return {"status": 200, "msg": _("Netease account logged in.")}, 200
else:
return {"status": 408, "msg": _("Netease account login failed.")}, 408
def qrcode(self):
self.session = ncm.login.LoginQrcodeUnikey()["unikey"]
loginURL = f"https://music.163.com/login?codekey={self.session}"
#return self._qrcode(loginURL)
return loginURL
def logout(self) -> dict:
self.status.cache_clear()
ncm.login.LoginLogout()
self.accountType = AccountType.ACCOUNT_NOLOGIN
return {"status": 200, "msg": _("Netease account logged out.")}, 200
@lru_cache(maxsize=8)
def status(self) -> dict:
return ncm.login.GetCurrentLoginStatus()
def userStatus(self) -> dict:
try:
status = self.status()
if self.accountType == AccountType.ACCOUNT_NOLOGIN:
return {"status": 200, "accountType": self.accountType, "username": "未登录"}
if self.accountType == AccountType.ACCOUNT_ANONYMOUS:
return {"status": 200, "accountType": self.accountType, "username": "匿名用户"}
return {"status": 200, "accountType": self.accountType, "username": status["profile"]["nickname"]}
except Exception as e:
print(e)
return {"status": 500, "msg": _("Get status failed, internal server error!")}
def loginAnonymous(self) -> dict:
self.status.cache_clear()
try:
ncm.login.LoginViaAnonymousAccount()
self.accountType = AccountType.ACCOUNT_ANONYMOUS
return {"status": 200, "msg": _("Netease anonymous account logged in.")}, 200
except:
return {"status": 408, "msg": _("Netease anonymous account login failed, timed out.")}, 408
def checkURL(self, content: str) -> bool:
for i in self.URLKeyword:
if i in content:
return True
return False
def parseURL(self, content: str) -> int or None:
uid = None
try:
if "music.163.com" in content:
uid = re.search(r"song\?id=[0-9]+",content).group()
uid = int(uid[8:])
elif "网易云" in content:
uid = re.search(r"song\?id=[0-9]+",content).group()
uid = int(uid[8:])
elif "163cn.tv" in content:
uid = httpx.get(content).headers.get("location")
uid = re.search(r"song\?id=[0-9]+",uid).group()
uid = int(uid[8:])
elif type(eval(content)) == type(1):
uid = eval(content)
except Exception as e:
print(e)
return None
return uid
def checkSong(self, content: int) -> SongStatus:
response = self._getSongStatus(content)
try:
if len(response["songs"]) == 0:
return SongStatus.SONG_NOTEXIST
if response["songs"][0]["fee"] == 1:
return SongStatus.SONG_ISVIP
return SongStatus.SONG_EXIST
except:
return SongStatus.SONG_NOTEXIST
return SongStatus.SONG_NOTEXIST
def getSongURL(self, content: int) -> str:
response = ncm.track.GetTrackAudio(content)
return response["data"][0]["url"]
def getSongImage(self, content: int) -> str:
response = self._getSongStatus(content)
return response["songs"][0]["al"]["picUrl"]
def getSongName(self, content: int) -> str:
response = self._getSongStatus(content)
name = response["songs"][0]["name"] + "-"
for i in response["songs"][0]["ar"]:
name += i["name"]
name += "/"
return name[:-1]
def getSongLyric(self, content: int) -> str:
#print(self._getSongStatus(content))
response = ncm.track.GetTrackLyrics(content)
return response['lrc']['lyric']
def dumpSession(self) -> str:
return pyncm.DumpSessionAsString(pyncm.GetCurrentSession())
def restoreSession(self, session: str):
session = pyncm.LoadSessionFromString(session)
pyncm.SetCurrentSession(session)
@lru_cache(maxsize=256)
def _getSongStatus(self, content: int) -> dict:
response = ncm.track.GetTrackDetail(content)
return response
if __name__ == "__main__":
ncmsess = NeteaseSession()
print(ncmsess.parseURL("分享心华的单曲《一人行者》: https://y.music.163.com/m/song?id=526116050&userid=1578177131&dlt=0846 (来自@网易云音乐)"))
# print(ncmsess.loginAnonymous())
# print(ncmsess.dumpSession())
# print(ncmsess.checkSong(1828241226))
print(ncmsess.getSongLyric(30953009))
# print(ncmsess.checkSong(2014291207))
# print(ncmsess.checkSong(1901371647))
# print(ncmsess.checkSong(1828241226))
# print(ncmsess.getSongName(1828241226))
# print(ncmsess.getSongImage(1828241226))
# print(ncmsess.getSongURL(1828241226))
# print(ncmsess.status())
# print(ncmsess._getSongStatus.cache_info())
+65
View File
@@ -0,0 +1,65 @@
import qrcode
from enum import Enum, StrEnum
from abc import abstractmethod, ABC
class SongStatus(Enum):
SONG_EXIST = 1
SONG_NOTEXIST = 0
SONG_NOTAVAIL = 2
SONG_ISVIP = 3
class AccountType(StrEnum):
ACCOUNT_NOLOGIN = "未登录"
ACCOUNT_ANONYMOUS = "匿名用户"
ACCOUNT_USER = "普通用户"
ACCOUNT_VIP = "VIP用户"
ACCOUNT_COOKIE = "Cookie登录"
def __json__(self):
return self.value
class SessionProvider():
tempPath = None
session = None
@abstractmethod
def login(self):
pass
@abstractmethod
def logout(self):
pass
@abstractmethod
def qrcode(self):
pass
def _qrcode(self, content):
raise DeprecationWarning("Please use string instead.")
if type(content)!=type("string"):
raise ValueError("QRCode content should be string")
img = qrcode.make(content)
return img
@abstractmethod
def status(self):
pass
@abstractmethod
def loginAnonymous(self):
pass
@abstractmethod
def checkSong(self, content: int):
pass
@abstractmethod
def parseURL(self, content: str):
pass
@abstractmethod
def dumpSession(self):
pass
@abstractmethod
def restoreSession(self, session: str):
pass
+99
View File
@@ -0,0 +1,99 @@
try:
from Provider import SessionProvider, SongStatus, AccountType
except:
from Extensions.Provider import SessionProvider, SongStatus, AccountType
from enum import Enum
import httpx
import re
from functools import lru_cache
try:
print(_)
except:
_ = lambda x: x
# 次奥,要手写API了
class QQMusicSession(SessionProvider):
session = None
providerName = "QQMusic"
URL_Keyword = ["y.qq.com"]
accountType = AccountType.ACCOUNT_NOLOGIN
cookies = {}
def __init__(self):
print(_("WARN: This extension is deprecated due to the change of APIkey of y.qq.com!"))
def loginAnonymous(self):
return {"status": 501, "msg": _("QQMusic does not support anonymous login.")}, 501
def loginCredential(self, content: str, key: str):
cookies = content
return {"status": 200, "msg": _("Cookies updated.")}
def login(self) -> dict:
if cookies:
self.accountType = AccountType.ACCOUNT_COOKIE
return {"status": 200, "msg": _("QQMusic login succeeded.")}
else:
return {"status": 401, "msg": _("Cookie required!")}, 401
def logout(self):
self.accountType = AccountType.ACCOUNT_NOLOGIN
self.cookies = ""
return {"status": 200, "msg": _("QQMusic logout succeeded.")}
def parseURL(self, content: str) -> str or None:
mid = None
try:
if "fcgi-bin/u?__" in content:
content = re.search(r"https://.*", content).group()
print(content)
mid = httpx.get(content).headers.get("location")
mid = re.search(r"songmid=(.+?)&", mid).group(1)
elif "ryqq/songDetail" in content:
mid = re.search(r"/[^/]+$", content).group()[1:]
except Exception as e:
print(e)
return None
return mid
def checkURL(self, content: str) -> bool:
for i in self.URLKeyword:
if i in content:
return True
return False
def getSongURL(self, content: str) -> str:
payload = r"https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&data={%22req_0%22:{%22module%22:%22vkey.GetVkeyServer%22,%22method%22:%22CgiGetVkey%22,%22param%22:{%22filename%22:[%22PREFIXSONGMIDSONGMID.SUFFIX%22],%22guid%22:%2210000%22,%22songmid%22:[%22SONGMID%22],%22songtype%22:[0],%22uin%22:%220%22,%22loginflag%22:1,%22platform%22:%2220%22}},%22loginUin%22:%220%22,%22comm%22:{%22uin%22:%220%22,%22format%22:%22json%22,%22ct%22:24,%22cv%22:0}}"
payload = payload.replace("PREFIX", "M500")
payload = payload.replace("SONGMID", content)
payload = payload.replace("SUFFIX", "mp3")
data = httpx.get(payload, cookies=self.cookies)
print(payload)
print(data.json())
print(a:=data.json()["req_0"]["data"]["midurlinfo"][0]["purl"])
print(b:=data.json()["req_0"]["data"]["sip"][0])
return b+a
@lru_cache(maxsize=256)
def _getSongStatus(self, content:str) -> dict:
payload = r"https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&data={%22req%22:{%22module%22:%22music.pf_song_detail_svr%22,%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22songmid%22:%22SONGMID%22}},%22loginUin%22:%220%22,%22comm%22:{%22uin%22:%220%22,%22format%22:%22json%22,%22ct%22:24,%22cv%22:0}}"
payload = payload.replace("SONGMID", content)
data = httpx.get(payload,cookies=self.cookies )
print(data.json())
def dumpSession(self) -> str:
return self.cookies
def restoreSession(self, session: str):
self.cookies = session
self.login()
if __name__ == "__main__":
cookies = "pac_uid=1_1599172402; iip=0; _qimei_fingerprint=a60e880d499fb09dfa3fc2e7b67f5cac; _qimei_q36=; _qimei_h38=5261d3497d310405f78852440900000df17a1e; RK=vx0t0LFmeo; ptcz=ad36d5ca2a0e7a6b7250c853a187f263446dfb2530128858c1c61c5fcc10875c; pgv_pvid=2920262853; eas_sid=d14770F6p8X5a1M6V2z6X8H9W1; qq_domain_video_guid_verify=c6c04001d0488e55; o_cookie=1599172402; suid=ek168787168502135101; _qimei_uuid42=18401162425100258014e7193532ca71547ede2d09; vversion_name=8.2.95; pgv_info=ssid=s4743344675; uin=1599172402; fqm_pvqid=d34c53e5-031d-4762-b0e1-10a3b13013b6; fqm_sessionid=12a505d3-2ba4-4fd8-b025-6d773e4153e6; ts_refer=cn.bing.com/; ts_uid=2582842080; ts_last=y.qq.com/n/ryqq/songDetail/002vbWwe3ZmffT; login_type=1; psrf_qqaccess_token=2BFFE211C6375E16CAE328C65066052A; qm_keyst=Q_H_L_63k3Naa-qgZKZwiv-Q6TaqMCUYqy0nrKuDW6p2NbmOYb0AYcnh1ttAqA9Az9tpu2D1FCrAkeJFQ; wxunionid=; euin=oK4qNK6lowvzoc**; psrf_qqrefresh_token=B75229616609EF6988510371BEF3EFA2; psrf_qqunionid=9A90D76C7D610B8DF336DC5F19EC8773; qqmusic_key=Q_H_L_63k3Naa-qgZKZwiv-Q6TaqMCUYqy0nrKuDW6p2NbmOYb0AYcnh1ttAqA9Az9tpu2D1FCrAkeJFQ; tmeLoginType=2; music_ignore_pskey=202306271436Hn@vBj; psrf_musickey_createtime=1714623437; wxrefresh_token=; psrf_access_token_expiresAt=1722399437; psrf_qqopenid=027F5F42CADC5D52F710B2936B81B0C5; wxopenid="
qmsess = QQMusicSession()
smid = qmsess.parseURL("https://c6.y.qq.com/base/fcgi-bin/u?__=yCNzlNSNbaWD")
smid = qmsess.parseURL("https://y.qq.com/n/ryqq/songDetail/000lKV2G0PEkc3")
print(qmsess.loginCredential(cookies, ""))
print(qmsess.login())
print(qmsess.getSongURL(smid))
#print(qmsess._getSongStatus(smid))
+33
View File
@@ -0,0 +1,33 @@
import re
import time
def lrcFormat(content: str) -> str:
lrcTuple = []
result = ""
def strTimeToFloat(content: str) -> float:
t = re.findall(r"\d+", content)
if len(t) == 3:
return int(t[0])*60 + int(t[1]) + float(t[2])/(10**(len(t[2])))
elif len(t) == 2:
return int(t[0])*60 + float(t[1])
def floatToStrTime(content: float) -> str:
return f"{int(content//60):02d}:{int(content%60):02d}.{int(content*1000%1000):03d}"
# 读取歌词
if content is None:
return "[0.0] 该歌曲暂无歌词\n"
for i in content.split('\n'):
a = re.findall(r"\[.*?\]",i)
timestamps = [strTimeToFloat(i[1:-1]) for i in a]
if len(timestamps) == 0:
continue
c = re.sub(r"\[.*?\]", "", i).strip()
for i in timestamps:
lrcTuple.append((i, c))
lrcTuple.sort(key=lambda x:x[0])
for i in lrcTuple:
result += "[" + str(i[0]) + "] " + i[1] + "\n"
return result
if __name__ == "__main__":
data = open("test.lrc", "r").read()
open("result.lrc", "w").write(lrcFormat(data))
+61
View File
@@ -0,0 +1,61 @@
import httpx
from Music import Music
import uuid
from Extensions.Netease import NeteaseSession
from Extensions.Provider import SessionProvider
from mutagen import mp3, flac
providers = []
def downloadMusic(music: Music, path: str) -> dict:
musicUUID = str(uuid.uuid4())
filename = path + "/" + musicUUID + ".mp3"
imagefilename = path + "/" + musicUUID + ".jpg"
lyricfilename = path + "/" + musicUUID + ".lrc"
try:
with httpx.stream("GET", music.musicURL) as r:
with open(filename, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
with httpx.stream("GET", music.imageURL) as r:
with open(imagefilename, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
with open(lyricfilename, "wb") as f:
f.write(music.lyric.encode("utf-8")) # 仅测试用途
except PermissionError:
return {"status": 500, "msg": _("File saving failed, permission denied!")}, 500
except FileNotFoundError:
return {"status": 500, "msg": _("File saving failed, path doesn't exist!")}, 500
except:
return {"status": 500, "msg": _("File saving failed, internal server error!")}, 500
music.uuid = musicUUID
return {"status": 200, "msg": _("File saved."), "filename": filename, "uuid": musicUUID}, 200
def judgeProvider(provider: str, content: str, providerList: list) -> SessionProvider or None :
for i in providerList:
if provider == i.providerName:
return i
if i.checkURL(content):
return i
return None
def imagePayload(music) -> bytes or None:
with open(music, "rb") as file:
if(music.endswith("mp3")):
target = mp3.MP3(file)
return target.tags['APIC:'].data
elif(music.endswith("flac")):
target = flac.FLAC(file)
return target.pictures[0].data
else:
raise AttributeError(_("Unsupported file format."))
raise AttributeError(_("File not found."))
if __name__ == "__main__":
#ncmsession = NeteaseSession()
#print(judgeProvider(None, "https://music.163.com/song?id=114514", [ncmsession]))
#exit(0)
mu01 = Music("pig2014", None, "My Ordinary Life", "Netease", "https://p2.music.126.net/E_oSaVzztyW0yg-5IvklCg==/109951165802120160.jpg", "http://m701.music.126.net/20240402195141/3b62ce359fc591ca8857a2dd0b9c6909/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/28531063631/77f4/653d/bdf8/a43f2583f10ce9f41046586a19a19c77.mp3")
print(downloadMusic(mu01, "/tmp/ttmp"))
#print(mu01.dict())
+64
View File
@@ -0,0 +1,64 @@
import Music
import pickle
class MusicQueue():
queue = []
def __init__(self):
self.queue = []
def push(self, element: Music):
self.queue.append(element)
def pop(self):
self.queue.pop(0)
def front(self) -> Music:
return self.queue[0]
def clear(self):
self.queue = []
def elevate(self, target: Music):
# elevate target to 1st place
self.queue.remove(target)
self.queue.insert(0, target)
def find(self, target: Music) -> Music or None:
for i in self.queue:
if i == target:
return i
return None
def count(self, target: Music):
count = 0
for i in self.queue:
if i == target:
count += 1
return count
def findAndElevate(self, target: Music):
target = self.find(target)
if target is not None:
self.elevate(target)
def delete(self, target: Music):
target = self.find(target)
if target is not None:
self.queue.remove(target)
def size(self):
return len(self.queue)
def dict(self):
return [i.dict() for i in self.queue]
def __str__(self):
return str(self.queue)
def dump(self):
return pickle.dumps(self.queue)
def load(self, data: bytes):
self.queue = pickle.loads(data)
+18
View File
@@ -0,0 +1,18 @@
data = {}
def create(username: str, stardust: int, nextCost: int):
data[username] = (stardust, nextCost)
def read(username: str):
return data[username]
def check(username: str):
return username in data
def update(username: str, stardust: int, nextCost: int):
data[username] = (stardust, nextCost)
def smartread(username: str, stardust: int, nextCost: int):
if username not in data:
create(username, stardust, nextCost)
return data[username]
+53
View File
@@ -0,0 +1,53 @@
from flask import Flask, request, send_from_directory
from dynaconf import FlaskDynaconf
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, current_user, login_user, verify_password, roles_accepted, logout_user
from flask_security.models import fsqla_v3 as fsqla
from flask_socketio import SocketIO, join_room, leave_room, emit
from flask_wtf import CSRFProtect
import sys
app = Flask("GalaxyMusicHall")
FlaskDynaconf(app, settings_files=["instance/config.toml", "instance/.secrets.toml"], environments=True)
db = SQLAlchemy(app)
fsqla.FsModels.set_db_info(db)
class Role(db.Model, fsqla.FsRoleMixin):
pass
class User(db.Model, fsqla.FsUserMixin):
pass
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
CSRFProtect(app)
app.security = Security(app, user_datastore)
if __name__ == "__main__":
if len(sys.argv) > 1:
with app.app_context():
print("初始化数据库...")
db.create_all()
app.security.datastore.find_or_create_role(name="admin")
app.security.datastore.find_or_create_role(name="user")
app.security.datastore.find_or_create_role(name="player")
with open(sys.argv[1], "r", encoding="utf-8") as file:
for i in file.readlines():
username, password = i.strip().split(',')
print(username,password)
if not app.security.datastore.find_user(username=username):
app.security.datastore.create_user(username=username, email= username+"@null.com", password=hash_password(password))
user = app.security.datastore.find_user(username=username)
app.security.datastore.add_role_to_user(user, "user")
db.session.commit()
else:
print("数据库连接有效")
print("请在参数1指定.csv文件用于导入用户")
+75
View File
@@ -0,0 +1,75 @@
[default]
# 指定密码哈希
security_password_hash = "argon2"
# 设置cookie SameSite属性
remember_cookie_samesite = "strict"
session_cookie_samesite = "strict"
# 允许推送
enable_push = true
# 设置用户推送限制
user_pushlimit = 2
# 设置总推送限制
total_pushlimit = 60
# 设置是否允许重复推送
allow_duplicate_push = false
# 实例名称,用于在前端展示
instance_name = "银河音乐厅Next_v1.0.0"
# 缓存路径,存放下载的歌曲文件与封面等
cache_path = "/tmp/galaxycache"
# 是否保留缓存,若为false,则每次启动时都会清空缓存
preserve_cache = true
# 是否保存会话,上一选项为false时无效
save_session = false
# 喵化API的"msg"字段输出
meowlize = false
# 启用用户歌曲提升(消耗星尘)
# 若不启用,则下述三个参数无效
allow_user_elevation = true
# 设置用户初始星尘数
initial_stardust = 100
# 设置初始星尘扣除数
initial_stardust_cost = 10
# 设置星尘扣除递增数
stardust_cost_increase = 10
# 设置上传歌曲允许的后缀
allowed_upload_music = ["mp3", "flac", "wav", "aac"]
# 设置上传歌词允许的后缀
allowed_upload_lyric = ["lrc"]
# 设置上传歌曲的默认图标
upload_music_image = "https://www.pngall.com/wp-content/uploads/2/Upload-PNG-Image.png"
# 允许播放器端登录网易云帐号
allow_player_netease_login = false
# 以下为使Flask-Security工作必须的选项
MAIL_DEFAULT_SENDER = "catty"
SECURITY_EMAIL_SENDER = "catty"
SECURITY_CSRF_PROTECT_MECHANISMS = ""
WTF_CSRF_CHECK_DEFAULT = false
# 以下为使自定义登录API工作必须的选项
SECURITY_LOGIN_URL = "/hiddenapi/login"
SECURITY_LOGOUT_URL = "/hiddenapi/logout"
[development]
# 只用来测试
debug = 1
host = "127.0.0.1"
port = 5000
cache_path = "/tmp/galaxycache"
banner_file = "./banner.md"
SQLALCHEMY_DATABASE_URI = "sqlite:///data.db"
[production]
# 正式部署
debug = 0
host = "0.0.0.0"
port = 5000
cache_path = "/tmp/galaxycache"
banner_file = "./banner.md"
SQLALCHEMY_DATABASE_URI = "sqlite:///data_prod.db"
BIN
View File
Binary file not shown.
Binary file not shown.
+159
View File
@@ -0,0 +1,159 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-02 20:45+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: python\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: main.py:55
msgid "Not authenticated!"
msgstr "请登录帐号喵!"
#: main.py:58
msgid "Permission denied!"
msgstr "权限不足喵!"
#: main.py:65
msgid "Hello, this is the API testing page of galaxy music hall."
msgstr "你好,这里是银河音乐厅的API测试页面喵。"
#: main.py:69
msgid "Server is running"
msgstr "服务器正在运行喵。"
#: main.py:81 main.py:86 main.py:94
msgid "Incorrect user or password!"
msgstr "用户名或密码错误喵!"
#: main.py:90
msgid "Login success."
msgstr "登录成功喵。"
#: main.py:92
msgid "Login failed, unknown exception."
msgstr "登录失败,未知异常喵!"
#: main.py:119
msgid "No music in queue."
msgstr "队列里是空的喵。"
#: main.py:102
msgid "Push parameter is empty!"
msgstr "推送参数是空的喵!"
#: main.py:149
msgid "Unsupported music provider!"
msgstr "不支持这个提供者喵!"
#: main.py:154
msgid "The music does not exist!"
msgstr "歌曲不存在喵!"
#: main.py:156
msgid "The music is not available!"
msgstr "歌曲不可用喵!"
#: main.py:158
msgid "The music is VIP only, please contact administrator to login VIP account!"
msgstr "歌曲是VIP音乐,请联系管理员登录VIP帐号喵!"
#: main.py:164 main.py:233
msgid "The music is already in the queue!"
msgstr "歌曲已经在队列里了喵!"
#: main.py:169
msgid "Successfully pushed the music."
msgstr "成功推送喵。"
#: main.py:179 main.py:187 main.py:199 main.py:211
msgid "The music is not in the queue!"
msgstr "歌曲不在队列里喵!"
#: main.py:181 main.py:201
msgid "You are not the owner of the music!"
msgstr "你不是歌曲的拥有者喵!"
#: main.py:183 main.py:189
msgid "Successfully deleted the music."
msgstr "成功删除喵。"
#: main.py:203
msgid "You do not have enough stardust to elevate the music!"
msgstr "你没有足够的星尘来提升歌曲喵!"
#: main.py:207 main.py:213
msgid "Successfully elevated the music."
msgstr "成功提升喵。"
#: main.py:229
msgid "No file selected!"
msgstr "没有选择文件喵!"
#: main.py:231
msgid "Invalid file format!"
msgstr "无效的文件格式喵!"
#: main.py:239
msgid "Failed to save the file!"
msgstr "无法保存文件喵!"
#: main.py:242
msgid "Successfully uploaded the music!"
msgstr "成功上传歌曲喵。"
#: main.py:258 main.py:279
msgid "Failed to reload the banner!"
msgstr "无法加载Banner喵!"
#: main.py:277
msgid "Permission denied to access cache directory!"
msgstr "没有访问缓存目录的权限喵!"
#: MusicManager.py:23
msgid "File saving failed, permission denied!"
msgstr "文件保存失败喵!(权限不够喵...)"
#: MusicManager.py:25
msgid "File saving failed, path doesn't exist!"
msgstr "文件保存失败喵!(路径不存在喵...)"
#: MusicManager.py:27
msgid "File saving failed, internal server error!"
msgstr "文件保存失败喵!(服务器出错了喵...)"
#: MusicManager.py:29
msgid "File saved."
msgstr "文件保存了喵。"
#: Extensions/Netease.py:27
msgid "Netease account logged in."
msgstr "网易云帐号登录成功喵。"
#: Extensions/Netease.py:29
msgid "Netease account login failed."
msgstr "网易云帐号登录失败喵。"
#: Extensions/Netease.py:39
msgid "Netease account logged out."
msgstr "网易云帐号登出了喵。"
#: Extensions/Netease.py:48
msgid "Netease anonymous account logged in."
msgstr "网易云匿名帐号登录成功喵。"
#: Extensions/Netease.py:50
msgid "Netease anonymous account login failed, timed out."
msgstr "网易云帐号登录失败喵。(超时了喵)"
Binary file not shown.
+158
View File
@@ -0,0 +1,158 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-02 20:45+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: python\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: main.py:55
msgid "Not authenticated!"
msgstr "请登录帐号!"
#: main.py:58
msgid "Permission denied!"
msgstr "权限不足!"
#: main.py:65
msgid "Hello, this is the API testing page of galaxy music hall."
msgstr "你好,这里是银河音乐厅的API测试页面。"
#: main.py:69
msgid "Server is running"
msgstr "服务器正在运行。"
#: main.py:81 main.py:86 main.py:94
msgid "Incorrect user or password!"
msgstr "用户名或密码错误!"
#: main.py:90
msgid "Login success."
msgstr "登录成功。"
#: main.py:92
msgid "Login failed, unknown exception."
msgstr "登录失败,未知异常!"
#: main.py:119
msgid "No music in queue."
msgstr "队列为空。"
#: main.py:102
msgid "Push parameter is empty!"
msgstr "推送参数为空!"
#: main.py:149
msgid "Unsupported music provider!"
msgstr "不支持这个提供者!"
#: main.py:154
msgid "The music does not exist!"
msgstr "歌曲不存在!"
#: main.py:156
msgid "The music is not available!"
msgstr "歌曲不可用!"
#: main.py:158
msgid "The music is VIP only, please contact administrator to login VIP account!"
msgstr "歌曲是VIP音乐,请联系管理员登录VIP帐号!"
#: main.py:164 main.py:233
msgid "The music is already in the queue!"
msgstr "歌曲已经在队列里了!"
#: main.py:169
msgid "Successfully pushed the music."
msgstr "成功推送。"
#: main.py:179 main.py:187 main.py:199 main.py:211
msgid "The music is not in the queue!"
msgstr "歌曲不在队列里!"
#: main.py:181 main.py:201
msgid "You are not the owner of the music!"
msgstr "你不是歌曲的拥有者!"
#: main.py:183 main.py:189
msgid "Successfully deleted the music."
msgstr "成功删除。"
#: main.py:203
msgid "You do not have enough stardust to elevate the music!"
msgstr "你没有足够的星尘来提升歌曲!"
#: main.py:207 main.py:213
msgid "Successfully elevated the music."
msgstr "成功提升。"
#: main.py:229
msgid "No file selected!"
msgstr "没有选择文件!"
#: main.py:231
msgid "Invalid file format!"
msgstr "无效的文件格式!"
#: main.py:239
msgid "Failed to save the file!"
msgstr "无法保存文件!"
#: main.py:242
msgid "Successfully uploaded the music!"
msgstr "成功上传歌曲。"
#: main.py:258 main.py:279
msgid "Failed to reload the banner!"
msgstr "无法加载Banner"
#: main.py:277
msgid "Permission denied to access cache directory!"
msgstr "没有访问缓存目录的权限!"
#: MusicManager.py:23
msgid "File saving failed, permission denied!"
msgstr "文件保存失败!(权限不够)"
#: MusicManager.py:25
msgid "File saving failed, path doesn't exist!"
msgstr "文件保存失败!(路径不存在)"
#: MusicManager.py:27
msgid "File saving failed, internal server error!"
msgstr "文件保存失败!(内部服务器错误)"
#: MusicManager.py:29
msgid "File saved."
msgstr "文件保存成功。"
#: Extensions/Netease.py:27
msgid "Netease account logged in."
msgstr "网易云帐号登录成功。"
#: Extensions/Netease.py:29
msgid "Netease account login failed."
msgstr "网易云帐号登录失败。"
#: Extensions/Netease.py:39
msgid "Netease account logged out."
msgstr "网易云帐号登出了。"
#: Extensions/Netease.py:48
msgid "Netease anonymous account logged in."
msgstr "网易云匿名帐号登录成功。"
#: Extensions/Netease.py:50
msgid "Netease anonymous account login failed, timed out."
msgstr "网易云帐号登录失败。(请求超时)"
+630
View File
@@ -0,0 +1,630 @@
import uuid
import os
import shutil
from flask import Flask, request, send_from_directory
from dynaconf import FlaskDynaconf
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, current_user, login_user, verify_password, roles_accepted, logout_user
from flask_security.models import fsqla_v3 as fsqla
from flask_socketio import SocketIO, join_room, leave_room, emit
from flask_wtf import CSRFProtect
import gettext
import warnings
import signal
import sys
import pickle
from MusicQueue import MusicQueue
from Music import Music
import MusicManager
import Stardust
import LyricFormatter
from Extensions.Netease import NeteaseSession
# from Extensions.QQMusic import QQMusicSession
# from Extensions.Bilibili import BilibiliSession
from Extensions.Provider import SongStatus, AccountType, SessionProvider
app = Fla325-576-113sk("GalaxyMusicHall")
socketio = SocketIO(app, cors_allowed_origins='*')
# 启用配置文件读取
FlaskDynaconf(app, settings_files=[
"instance/config.toml", "instance/.secrets.toml"], environments=True)
# 喵化输出
if app.config["meowlize"]:
os.environ["LANGUAGE"] = "meow"
# 初始化gettext
gettext.install('main', localedir='./locales')
db = SQLAlchemy(app)
fsqla.FsModels.set_db_info(db)
# 初始化扩展会话
ncmSession = NeteaseSession()
class Role(db.Model, fsqla.FsRoleMixin):
pass
class User(db.Model, fsqla.FsUserMixin):
pass
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
CSRFProtect(app)
app.security = Security(app, user_datastore)
queue = MusicQueue()
current = Music()
providerList = [ncmSession]
SOCKETIO_UPDATE_RESPONSE = {"status": 101, "msg": "该API使用SocketIO接口"}, 200
def unauthenticated(mechanisms=None, headers=None):
return {"status": 401, "msg": _("Not authenticated!")}, 401
def denied(mechanisms=None, headers=None):
return {"status": 403, "msg": _("Permission denied!")}, 403
app.security.unauthn_handler(unauthenticated)
app.security.unauthz_handler(denied)
def sigint_handler(signal, frame):
print(_("Quitting..."))
saveSession()
sys.exit(0)
signal.signal(signal.SIGINT, sigint_handler)
@app.route("/")
def test():
return {"status": 200, "msg": _("Hello, this is the API testing page of galaxy music hall."), "debug": app.debug}
@app.route("/ping", methods=["GET"])
def ping():
return {"message": _("Server is running"),
"instanceName": app.config.get("INSTANCE_NAME")}, 200
@app.route("/config", methods=["GET"])
@auth_required()
def getConfig():
return {"allowPlayerNeteaseLogin": app.config.get("ALLOW_PLAYER_NETEASE_LOGIN")}, 200
@app.route("/user/login", methods=["POST"])
def userLogin(): # SocketIO似乎不能写cookies
username = request.form.get("username")
if username is None:
return {"status": 403, "msg": _("Incorrect user or password!")}, 403
password = request.form.get("password")
try:
status = verify_password(
password, user_datastore.find_user(username=username).password)
if status is False:
return {"status": 403, "msg": _("Incorrect user or password!")}, 403
status = login_user(user_datastore.find_user(username=username), remember=True,
authn_via="password")
if status:
return {"status": 200, "msg": _("Login success.")}, 200
else:
return {"status": 400, "msg": _("Login failed, unknown exception.")}, 400
except AttributeError:
return {"status": 403, "msg": _("Incorrect user or password!")}, 403
except Exception as e:
print(e)
return {"status": 500, "msg": _("Login failed, internal server error.")}, 500
@app.route("/user/logout", methods=["POST"])
def userLogout():
try:
logout_user()
return {"status": 200, "msg": _("Logout success.")}
except:
return {"status": 500, "msg": _("Logout failed, internal server error.")}, 500
@socketio.on("userStatus")
def userStatus():
try:
stardust, nextCost = Stardust.smartread(
current_user.username, app.config["INITIAL_STARDUST"], app.config["INITIAL_STARDUST_COST"])
return {"status": current_user.is_authenticated,
"username": current_user.username,
"role": current_user.roles[0].name,
"stardust": stardust,
"nextCost": nextCost}
except Exception as e:
return {"status": False,
"username": None,
"role": None,
"stardust": -1,
"nextCost": 65536}
return SOCKETIO_UPDATE_RESPONSE
@app.route("/music/current", methods=["GET"])
@auth_required()
def getCurrentMusic():
return current.dict(), 200
@socketio.on("nextMusic")
def nextMusic():
global current
try:
current = queue.front()
if current.uuid == "KOBEBRYANT":
return
queue.pop()
updateQueue()
return {"status": 200}, 200
except:
current = Music()
updateQueue()
return {"status": 204, "msg": _("No music in queue.")}, 204
# @app.route("/music/queue", methods=["GET"])
@socketio.on("getQueue") # TODO: 设置未登录用户可见性
def getQueue():
socketio.emit("updateQueue", {
"current": current.dict(), "queue": queue.dict()})
def updateQueue():
emit("updateQueue", {"current": current.dict(),
"queue": queue.dict()}, broadcast=True, namespace="/")
saveSession()
def pushLimitCheck():
if app.config["ENABLE_PUSH"] is False:
return {"status": 403, "msg": _("Push is disabled!")}, 403
username = current_user.username
if not "admin" in current_user.roles:
count = queue.count(Music(owner=username))
if count >= app.config["USER_PUSHLIMIT"]:
return {"status": 420, "msg": _("You have reached the push limit!")}, 420
count = queue.size()
if count >= app.config["TOTAL_PUSHLIMIT"]:
return {"status": 429, "msg": _("The push queue is full!")}, 429
@socketio.on("pushMusic")
@auth_required()
def pushMusic(providerString, content): # TODO: 将下载改为异步API,允许查看进度 TODO: 修改musicURL逻辑
# 推送速率限制
if status := pushLimitCheck():
return status
# 阻止角色为"admin", "user"以外的用户推送
# SocketIO似乎不支持auth_required
if "admin" not in current_user.roles and "user" not in current_user.roles:
return {"status": 403, "msg": _("Permission denied!")}, 403
# 处理音乐提供者
if content is None:
return {"status": 400, "msg": _("Push parameter is empty!")}, 400
provider = MusicManager.judgeProvider(
providerString, content, providerList)
if provider is None:
return {"status": 400, "msg": _("Unknown source, please specify!")}, 400
# 由提供者提供服务
musicID = provider.parseURL(content)
musicStatus = provider.checkSong(musicID)
if musicStatus == SongStatus.SONG_NOTEXIST:
return {"status": 404, "msg": _("The music does not exist!")}, 404
if musicStatus == SongStatus.SONG_NOTAVAIL:
return {"status": 403, "msg": _("The music is not available!")}, 403
if musicStatus == SongStatus.SONG_ISVIP and provider.accountType != AccountType.ACCOUNT_VIP:
return {"status": 412, "msg": _("The music is VIP only, please contact administrator to login VIP account!")}, 403
musicName = provider.getSongName(musicID)
music = Music(owner=current_user.username, uuid=None, name=provider.getSongName(musicID), source=provider.providerName, imageURL=provider.getSongImage(
musicID), musicURL=provider.getSongURL(musicID), lyric=LyricFormatter.lrcFormat(provider.getSongLyric(musicID)))
# 检查是否已存在
if (not app.config["ALLOW_DUPLICATE_PUSH"]) and queue.find(Music(name=musicName)):
return {"status": 201, "msg": _("The music is already in the queue!")}, 201
# 下载歌曲
MusicManager.downloadMusic(music, app.config["CACHE_PATH"])
# 推送至队列
queue.push(music)
if not current.uuid:
nextMusic()
else:
updateQueue()
return {"status": 200, "msg": _("Successfully pushed the music.")}, 200
@socketio.on("deleteMusic")
@auth_required()
def deleteMusic(musicUUID):
# musicUUID = request.form.get("uuid")
if musicUUID == "KOBEBRYANT":
return {"status": 400, "msg": "Manba out!"}
music = queue.find(Music(uuid=musicUUID))
if not "admin" in current_user.roles:
# 用户删除部分
if music is None:
return {"status": 404, "msg": _("The music is not in the queue!")}, 404
if music.owner != current_user.username:
return {"status": 403, "msg": _("You are not the owner of the music!")}, 403
queue.delete(music)
updateQueue()
return {"status": 200, "msg": _("Successfully deleted the music.")}, 200
else:
# 管理员删除部分
if music is None:
return {"status": 404, "msg": _("The music is not in the queue!")}, 404
queue.delete(music)
updateQueue()
return {"status": 200, "msg": _("Successfully deleted the music.")}, 200
@socketio.on("elevateMusic")
@auth_required()
def elevateMusic(musicUUID):
# musicUUID = request.form.get("uuid")
music = queue.find(Music(uuid=musicUUID))
if musicUUID == "KOBEBRYANT":
return {"status": 400, "msg": "What can I say?"}
if not "admin" in current_user.roles:
# 用户提升部分
if music is None:
return {"status": 404, "msg": _("The music is not in the queue!")}, 404
if music.owner != current_user.username:
return {"status": 403, "msg": _("You are not the owner of the music!")}, 403
stardust, nextCost = Stardust.smartread(
current_user.username, app.config["INITIAL_STARDUST"], app.config["INITIAL_STARDUST_COST"])
if stardust < nextCost:
return {"status": 403, "msg": _("You do not have enough stardust to elevate the music!")}, 403
queue.elevate(music)
Stardust.update(current_user.username, stardust-nextCost,
nextCost+app.config["STARDUST_COST_INCREASE"])
updateQueue()
emit("updateUserStatus", userStatus())
return {"status": 200, "msg": _("Successfully elevated the music.")}, 200
else:
# 管理员提升部分
if music is None:
return {"status": 404, "msg": _("The music is not in the queue!")}, 404
queue.elevate(music)
updateQueue()
return {"status": 200, "msg": _("Successfully elevated the music.")}, 200
@app.route("/play/<url>")
@auth_required()
@roles_accepted("player")
def playMusic(url):
for i in app.config["ALLOWED_UPLOAD_MUSIC"]:
try:
os.stat(app.config["CACHE_PATH"]+"/"+url+"."+i)
return send_from_directory(app.config["CACHE_PATH"], url + "." + i)
except Exception as e:
pass
return {"data": None}
@app.route("/image/<url>")
@auth_required()
def getImage(url):
for i in ("jpg", "png"):
try:
os.stat(app.config["CACHE_PATH"]+"/"+url+"."+i)
return send_from_directory(app.config["CACHE_PATH"], url + "." + i)
except Exception as e:
print(e)
pass
return "No such file or directory!", 404
@app.route("/lyric/<url>")
@auth_required()
def getLyric(url):
return send_from_directory(app.config["CACHE_PATH"], url+".lrc")
@app.route("/download")
def downloadMusic():
url = request.args.get("uuid")
if url == "flag":
return "用户拿flag,用户坏!", 400
for i in app.config["ALLOWED_UPLOAD_MUSIC"]:
try:
os.stat(app.config["CACHE_PATH"] + url + "." + i)
return send_from_directory(app.config["CACHE_PATH"], url + "." + i)
except:
pass
path = app.config["CACHE_PATH"] + "/" + url
try:
with open(path, "r", encoding="utf-8") as f:
data = f.read()
if "flag" in data:
return "flag{****REDACTED****}"
else:
return data
except Exception as e:
print(e)
return "无法下载此音乐!", 400
@socketio.on("adminResetQueue")
@auth_required()
def adminResetQueue():
global current
return {"status": 403, "msg": "已禁用此功能!"}, 403
if not "admin" in current_user.roles:
return {"status": 403, "msg": _("Permission denied!")}, 403
queue.clear()
current = Music()
updateQueue()
return {"status": 200, "msg": _("Reset success!")}
@app.route("/upload", methods=["GET", "POST"])
@roles_accepted("user", "admin")
def uploadMusic(): # TODO: 修改musicURL逻辑
# 推送速率限制
if status := pushLimitCheck():
return status
# 上传处理
file = request.files.get("file")
if file is None or file.filename == "":
# TODO: 空文件判断
return
if ".php" in file.filename:
return {"status": 200, "msg": "我觉得银河音乐厅不是个php应用..."}
# 是音乐文件
if file.filename.endswith(tuple(app.config["ALLOWED_UPLOAD_MUSIC"])):
suffix = file.filename.split(".")[-1]
if queue.find(Music(name=file.filename)):
return {"status": 201, "msg": _("The music is already in the queue!")}, 201
musicUUID = str(uuid.uuid4())
uploadFile = os.path.join(
app.config["CACHE_PATH"], musicUUID+"."+suffix)
uploadAlbum = app.config["UPLOAD_MUSIC_IMAGE"]
try:
file.save(uploadFile)
except Exception as e:
return {"status": 500, "msg": _("Failed to save the file!")}, 500
try:
album = MusicManager.imagePayload(uploadFile)
if album is not None:
albumFileName = os.path.join(
app.config["CACHE_PATH"], musicUUID+".jpg")
with open(albumFileName, "wb") as albumFile:
albumFile.write(album)
uploadAlbum = "/api/image/" + musicUUID
except Exception as e:
print(e)
warnings.warn(_("Failed to read the album image!"))
music = Music(owner=current_user.username, uuid=musicUUID, name=file.filename, source="Upload",
imageURL=uploadAlbum, musicURL=uploadFile, lyric=LyricFormatter.lrcFormat(None))
# TODO: 为上传的音乐添加歌词
queue.push(music)
if not current.uuid:
nextMusic()
else:
updateQueue()
return {"status": 200, "msg": _("Successfully uploaded the music!")}, 200
# 是歌词文件
elif file.filename.endswith(tuple(app.config["ALLOWED_UPLOAD_LYRIC"])):
return {"status": 501, "msg": _("Lyric file is not supported yet!")}, 501
else:
return {"status": 400, "msg": _("Invalid file format!"), "allowed_format": app.config["ALLOWED_UPLOAD_MUSIC"]}, 400
@socketio.on("getBanner")
def getBanner():
return {"data": open(app.config["CACHE_PATH"]+"/banner.md").read()}
@socketio.on("adminPushMessage")
def broadcastMessage(message: str):
emit("broadcastMessage", {"title": "来自服务器的消息", "content": message,
"duration": 5000}, broadcast=True, namespace="/")
def loadBanner(reload=0, bannerfile=app.config["BANNER_FILE"]):
regenerateUUID()
try:
with open(bannerfile, encoding="utf-8") as bannerfile:
bannerContent = bannerfile.read()
bannerContent = str.replace(
bannerContent, r"{{instanceName}}", "InstanceName: "+app.config["INSTANCE_NAME"])
bannerContent = str.replace(
bannerContent, r"{{instanceUUID}}", "InstanceUUID: "+app.config["INSTANCE_UUID"])
with open(app.config["CACHE_PATH"]+"/banner.md", "w", encoding="utf-8") as banner:
banner.write(bannerContent)
if reload:
emit('updateBanner', getBanner(), broadcast=True)
return {"status": 200, "msg": _("Successfully reloaded the banner!")}, 200
except Exception as e:
print(e)
return {"status": 500, "msg": _("Failed to reload the banner!")}, 500
@socketio.on("adminUpdateBanner")
@app.route("/admin/updateBanner", methods=["POST"])
@roles_accepted("admin")
def updateBanner():
file = request.files.get("file")
file.save(app.config["CACHE_PATH"]+"/new_banner.md")
return loadBanner(reload=0, bannerfile=app.config["CACHE_PATH"]+"/new_banner.md")
@socketio.on("adminResetBanner")
def resetBanner():
return loadBanner(reload=0)
@socketio.on("adminDisablePush")
def disablePush():
if not "admin" in current_user.roles:
return {"status": 403, "msg": _("Permission denied!")}, 403
if app.config["ENABLE_PUSH"]:
app.config["ENABLE_PUSH"] = False
emit("updatePushStatus",
(app.config["ENABLE_PUSH"], 1), broadcast=True, namespace="/")
return {"status": 200, "msg": _("Push disabled.")}
else:
app.config["ENABLE_PUSH"] = True
emit("updatePushStatus",
(app.config["ENABLE_PUSH"], 1), broadcast=True, namespace="/")
return {"status": 200, "msg": _("Push enabled.")}
@socketio.on("extCredential")
def extCredential(providerString: str, content: str, key: str):
provider = MusicManager.judgeProvider(providerString, "", providerList)
if not provider:
return {"status": 400, "msg": _("Invalid provider!")}, 400
return provider.loginCredential(content, key)
@app.route("/ext/login", methods=["POST"])
@socketio.on("extLogin")
def extLogin(providerString: str) -> dict:
provider = MusicManager.judgeProvider(providerString, "", providerList)
if not provider:
return {"status": 400, "msg": _("Invalid provider!")}, 400
status = provider.login()
updateExtStatus()
return status
@socketio.on("extAnonymousLogin")
def extAnonymousLogin(providerString: str) -> dict:
provider = MusicManager.judgeProvider(providerString, "", providerList)
if not provider:
return {"status": 400, "msg": _("Invalid provider!")}, 400
status = provider.loginAnonymous()
updateExtStatus()
return status
@app.route("/ext/qrcode", methods=["GET"])
@socketio.on("extQRCode")
def getExtQRCode(providerString: str) -> dict:
provider = MusicManager.judgeProvider(providerString, "", providerList)
if not provider:
return {"status": 400, "msg": _("Invalid provider!")}, 400
qrcode = provider.qrcode()
return {"status": 200, "url": qrcode}
def getExtStatus(provider: SessionProvider):
if not provider:
return {"status": 400, "msg": _("Invalid provider!")}, 400
return provider.userStatus()
def updateExtStatus():
for i in providerList:
print(i.providerName, getExtStatus(i))
emit("updateExtStatus", (i.providerName, getExtStatus(i)))
@socketio.on("init")
def clientInit():
emit("updateBanner", getBanner())
emit("updatePushStatus", (app.config["ENABLE_PUSH"], 0))
if "admin" in current_user.roles:
updateExtStatus()
def regenerateUUID():
app.config["INSTANCE_UUID"] = str(uuid.uuid1())
def init():
with app.app_context():
# # 若不存在数据库就创建
print(_("Creating database if not present..."))
db.create_all()
db.session.commit()
regenerateUUID()
try:
if app.config["PRESERVE_CACHE"]:
os.stat(app.config["CACHE_PATH"])
else:
shutil.rmtree(app.config["CACHE_PATH"])
os.mkdir(app.config["CACHE_PATH"])
except FileNotFoundError:
os.mkdir(app.config["CACHE_PATH"])
except PermissionError:
raise Exception(_("Permission denied to access cache directory!"))
except:
raise Exception(_("Failed to rebuild the cache directory!"))
os.system("pwd")
shutil.copy("./resource/flag.lrc", app.config["CACHE_PATH"])
shutil.copy("./resource/flag.wav", app.config["CACHE_PATH"])
shutil.copy("./resource/flag.png", app.config["CACHE_PATH"])
shutil.copy("./resource/KOBEBRYANT.jpg", app.config["CACHE_PATH"])
if loadBanner(reload=0)[1] != 200:
raise Exception(_("Failed to reload the banner!"))
if app.config["SAVE_SESSION"]:
if app.config["PRESERVE_CACHE"]:
try:
for i in providerList:
with open(app.config["CACHE_PATH"]+"/"+i.providerName+".session", "r", encoding="utf-8") as sessionFile:
i.restoreSession(sessionFile.read())
with open(app.config["CACHE_PATH"]+"/queue.pickle", "rb") as queueFile:
queue.load(queueFile.read())
with open(app.config["CACHE_PATH"]+"/current.pickle", "rb") as currentFile:
current = pickle.loads(currentFile.read())
except FileNotFoundError:
warnings.warn(_("No session found, skipping..."))
except Exception as e:
print(e)
raise Exception(_("Failed to restore the session!"))
else:
warnings.warn(
_("SAVE_SESSION doesn't work without PRESERVE_CACHE!"))
print(_("Successfully initialized the application!"))
def saveSession():
if app.config["SAVE_SESSION"]:
if not app.config["PRESERVE_CACHE"]:
warnings.warn(
_("SAVE_SESSION doesn't work without PRESERVE_CACHE!"))
try:
# 保存音乐提供者session
for i in providerList:
sessionText = i.dumpSession()
with open(app.config["CACHE_PATH"]+"/"+i.providerName+".session", "w", encoding="utf-8") as sessionFile:
sessionFile.write(sessionText)
# 保存歌曲队列
with open(app.config["CACHE_PATH"]+"/queue.pickle", "wb") as queueFile:
queueFile.write(queue.dump())
# 保存当前播放
with open(app.config["CACHE_PATH"]+"/current.pickle", "wb") as currentFile:
currentFile.write(pickle.dumps(current))
except Exception as e:
print(e)
warnings.warn(_("Failed to save the session, operation aborted."))
init()
if __name__ == "__main__":
socketio.run(app, debug=app.config["DEBUG"],
host=app.config["HOST"], port=app.config["PORT"])
+16
View File
@@ -0,0 +1,16 @@
dynaconf==3.2.5
Flask==3.0.2
Flask_Security_Too==5.4.3
Flask_SocketIO==5.3.6
flask_sqlalchemy==3.1.1
flask_wtf==1.2.1
httpx==0.27.0
pyncm==1.6.15
qrcode==7.4.2
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
mutagen==1.47.0
gunicorn==22.0
gevent==24.2.1
gevent-websocket==0.10.1
#bilibili-api-python==16.2.0