From 71b789fbaa7c755a95382cd4a4a3f6044a186639 Mon Sep 17 00:00:00 2001 From: A-random-githuber <89432931+A-random-githuber@users.noreply.github.com> Date: Fri, 26 Dec 2025 00:26:22 +0700 Subject: [PATCH 1/4] Song cache --- latest version/core/download.py | 213 +++++++++++++++++++++++++++++--- latest version/core/init.py | 3 +- 2 files changed, 197 insertions(+), 19 deletions(-) diff --git a/latest version/core/download.py b/latest version/core/download.py index 7caaf6b..5858d2d 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -1,4 +1,6 @@ import os +import sqlite3 +import threading from functools import lru_cache from json import loads from time import time @@ -13,12 +15,171 @@ from .user import User from .util import get_file_md5, md5 +class SongFileCache: + _local = threading.local() + _schema_lock = threading.Lock() + + @classmethod + def db_path(cls) -> str: + return os.path.join(os.path.dirname(Config.SQLITE_DATABASE_PATH) or '.', 'song_cache.db') + + @classmethod + def _conn(cls) -> sqlite3.Connection: + conn = getattr(cls._local, 'conn', None) + if conn is not None: + return conn + os.makedirs(os.path.dirname(cls.db_path()) or '.', exist_ok=True) + conn = sqlite3.connect(cls.db_path(), isolation_level=None, check_same_thread=False) + conn.row_factory = sqlite3.Row + cls._local.conn = conn + return conn + + @classmethod + def ensure_schema(cls) -> None: + with cls._schema_lock: + c = cls._conn() + c.execute('PRAGMA journal_mode=WAL;') + c.execute('PRAGMA synchronous=NORMAL;') + c.execute('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);') + c.execute('CREATE TABLE IF NOT EXISTS songs (song_id TEXT PRIMARY KEY, dir_mtime_ns INTEGER NOT NULL, last_scan INTEGER NOT NULL);') + c.execute('CREATE TABLE IF NOT EXISTS files (song_id TEXT NOT NULL, file_name TEXT NOT NULL, size INTEGER NOT NULL, mtime_ns INTEGER NOT NULL, md5 TEXT, last_seen INTEGER NOT NULL, PRIMARY KEY (song_id, file_name));') + c.execute('CREATE INDEX IF NOT EXISTS idx_files_song_id ON files(song_id);') + + @classmethod + def _delete_song(cls, song_id: str) -> None: + cls.ensure_schema() + c = cls._conn() + c.execute('DELETE FROM files WHERE song_id=?;', (song_id,)) + c.execute('DELETE FROM songs WHERE song_id=?;', (song_id,)) + + @classmethod + def _delete_file(cls, song_id: str, file_name: str) -> None: + cls.ensure_schema() + c = cls._conn() + c.execute('DELETE FROM files WHERE song_id=? AND file_name=?;', (song_id, file_name)) + + @classmethod + def sync_song(cls, song_id: str, dir_mtime_ns: int = None) -> None: + cls.ensure_schema() + song_dir = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id) + if not os.path.isdir(song_dir): + cls._delete_song(song_id) + return + if dir_mtime_ns is None: + dir_mtime_ns = os.stat(song_dir).st_mtime_ns + c = cls._conn() + row = c.execute('SELECT dir_mtime_ns FROM songs WHERE song_id=?;', (song_id,)).fetchone() + if row is not None and int(row['dir_mtime_ns']) == int(dir_mtime_ns): + return + now = int(time()) + c.execute('INSERT OR REPLACE INTO songs(song_id, dir_mtime_ns, last_scan) VALUES(?,?,?);', (song_id, int(dir_mtime_ns), now)) + file_names = [] + try: + for entry in os.scandir(song_dir): + if entry.is_file() and SonglistParser.is_available_file(song_id, entry.name): + file_names.append(entry.name) + except FileNotFoundError: + cls._delete_song(song_id) + return + if not file_names: + c.execute('DELETE FROM files WHERE song_id=?;', (song_id,)) + return + placeholders = ','.join(['?'] * len(file_names)) + c.execute(f'DELETE FROM files WHERE song_id=? AND file_name NOT IN ({placeholders});', (song_id, *file_names)) + for file_name in file_names: + path = os.path.join(song_dir, file_name) + if not os.path.isfile(path): + cls._delete_file(song_id, file_name) + continue + st = os.stat(path) + existing = c.execute('SELECT size, mtime_ns, md5 FROM files WHERE song_id=? AND file_name=?;', (song_id, file_name)).fetchone() + if existing is not None and int(existing['size']) == int(st.st_size) and int(existing['mtime_ns']) == int(st.st_mtime_ns): + if Config.SONG_FILE_HASH_PRE_CALCULATE and not existing['md5']: + c.execute('UPDATE files SET md5=?, last_seen=? WHERE song_id=? AND file_name=?;', (get_file_md5(path), now, song_id, file_name)) + else: + c.execute('UPDATE files SET last_seen=? WHERE song_id=? AND file_name=?;', (now, song_id, file_name)) + continue + md5_value = get_file_md5(path) if Config.SONG_FILE_HASH_PRE_CALCULATE else None + c.execute('INSERT OR REPLACE INTO files(song_id, file_name, size, mtime_ns, md5, last_seen) VALUES(?,?,?,?,?,?);', (song_id, file_name, int(st.st_size), int(st.st_mtime_ns), md5_value, now)) + + @classmethod + def sync_all(cls) -> None: + cls.ensure_schema() + root_dir = Constant.SONG_FILE_FOLDER_PATH + if not os.path.isdir(root_dir): + return + song_ids = [entry.name for entry in os.scandir(root_dir) if entry.is_dir() and entry.name != '.' and entry.name != '..'] + for song_id in song_ids: + song_dir = os.path.join(root_dir, song_id) + try: + dir_mtime_ns = os.stat(song_dir).st_mtime_ns + except FileNotFoundError: + continue + cls.sync_song(song_id, dir_mtime_ns) + c = cls._conn() + if song_ids: + placeholders = ','.join(['?'] * len(song_ids)) + c.execute(f'DELETE FROM songs WHERE song_id NOT IN ({placeholders});', song_ids) + c.execute(f'DELETE FROM files WHERE song_id NOT IN ({placeholders});', song_ids) + else: + c.execute('DELETE FROM songs;') + c.execute('DELETE FROM files;') + + @classmethod + def get_all_song_ids(cls, root_mtime_ns: int) -> list: + cls.ensure_schema() + c = cls._conn() + meta = c.execute('SELECT value FROM meta WHERE key="root_mtime_ns";').fetchone() + if meta is None or int(meta['value']) != int(root_mtime_ns): + cls.sync_all() + c.execute('INSERT OR REPLACE INTO meta(key, value) VALUES(?,?);', ('root_mtime_ns', str(int(root_mtime_ns)))) + return [row['song_id'] for row in c.execute('SELECT song_id FROM songs ORDER BY song_id;').fetchall()] + + @classmethod + def get_song_file_names(cls, song_id: str, dir_mtime_ns: int) -> list: + cls.ensure_schema() + c = cls._conn() + row = c.execute('SELECT dir_mtime_ns FROM songs WHERE song_id=?;', (song_id,)).fetchone() + if row is None or int(row['dir_mtime_ns']) != int(dir_mtime_ns): + cls.sync_song(song_id, dir_mtime_ns) + return [r['file_name'] for r in c.execute('SELECT file_name FROM files WHERE song_id=? ORDER BY file_name;', (song_id,)).fetchall()] + + @classmethod + def get_song_file_md5(cls, song_id: str, file_name: str, file_mtime_ns: int, file_size: int) -> str: + cls.ensure_schema() + path = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, file_name) + if not os.path.isfile(path): + cls._delete_file(song_id, file_name) + return None + c = cls._conn() + row = c.execute('SELECT size, mtime_ns, md5 FROM files WHERE song_id=? AND file_name=?;', (song_id, file_name)).fetchone() + now = int(time()) + if row is not None and int(row['size']) == int(file_size) and int(row['mtime_ns']) == int(file_mtime_ns) and row['md5']: + c.execute('UPDATE files SET last_seen=? WHERE song_id=? AND file_name=?;', (now, song_id, file_name)) + return row['md5'] + md5_value = get_file_md5(path) + c.execute('INSERT OR REPLACE INTO files(song_id, file_name, size, mtime_ns, md5, last_seen) VALUES(?,?,?,?,?,?);', (song_id, file_name, int(file_size), int(file_mtime_ns), md5_value, now)) + song_dir = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id) + if os.path.isdir(song_dir): + try: + c.execute('INSERT OR IGNORE INTO songs(song_id, dir_mtime_ns, last_scan) VALUES(?,?,?);', (song_id, int(os.stat(song_dir).st_mtime_ns), now)) + except FileNotFoundError: + pass + return md5_value + + @lru_cache(maxsize=8192) +def _get_song_file_md5_cached(song_id: str, file_name: str, file_mtime_ns: int, file_size: int) -> str: + return SongFileCache.get_song_file_md5(song_id, file_name, file_mtime_ns, file_size) + + def get_song_file_md5(song_id: str, file_name: str) -> str: path = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, file_name) if not os.path.isfile(path): + SongFileCache._delete_file(song_id, file_name) return None - return get_file_md5(path) + st = os.stat(path) + return _get_song_file_md5_cached(song_id, file_name, int(st.st_mtime_ns), int(st.st_size)) class SonglistParser: @@ -235,18 +396,15 @@ class DownloadList(UserDownload): def initialize_cache(cls) -> None: '''初始化歌曲数据缓存,包括md5、文件目录遍历、解析songlist''' SonglistParser() - if Config.SONG_FILE_HASH_PRE_CALCULATE: - x = cls() - x.url_flag = False - x.add_songs() - del x + SongFileCache.ensure_schema() + SongFileCache.sync_all() @staticmethod def clear_all_cache() -> None: '''清除所有歌曲文件有关缓存''' - get_song_file_md5.cache_clear() - DownloadList.get_one_song_file_names.cache_clear() - DownloadList.get_all_song_ids.cache_clear() + _get_song_file_md5_cached.cache_clear() + DownloadList._get_cached_song_file_names.cache_clear() + DownloadList._get_all_cached_song_ids.cache_clear() SonglistParser.songs = {} SonglistParser.pack_info = {} SonglistParser.free_songs = set() @@ -265,14 +423,22 @@ class DownloadList(UserDownload): @staticmethod @lru_cache(maxsize=2048) - def get_one_song_file_names(song_id: str) -> list: + def _get_cached_song_file_names(song_id: str, dir_mtime_ns: int) -> list: '''获取一个歌曲文件夹下的所有合法文件名,有lru缓存''' - r = [] - for i in os.scandir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)): - file_name = i.name - if i.is_file() and SonglistParser.is_available_file(song_id, file_name): - r.append(file_name) - return r + return SongFileCache.get_song_file_names(song_id, dir_mtime_ns) + + @staticmethod + def get_one_song_file_names(song_id: str) -> list: + song_dir = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id) + if not os.path.isdir(song_dir): + SongFileCache._delete_song(song_id) + return [] + try: + dir_mtime_ns = os.stat(song_dir).st_mtime_ns + except FileNotFoundError: + SongFileCache._delete_song(song_id) + return [] + return DownloadList._get_cached_song_file_names(song_id, int(dir_mtime_ns)) def add_one_song(self, song_id: str) -> None: @@ -321,9 +487,20 @@ class DownloadList(UserDownload): @staticmethod @lru_cache() - def get_all_song_ids() -> list: + def _get_all_cached_song_ids(root_mtime_ns: int) -> list: '''获取全歌曲文件夹列表,有lru缓存''' - return [i.name for i in os.scandir(Constant.SONG_FILE_FOLDER_PATH) if i.is_dir()] + return SongFileCache.get_all_song_ids(root_mtime_ns) + + @staticmethod + def get_all_song_ids() -> list: + root_dir = Constant.SONG_FILE_FOLDER_PATH + if not os.path.isdir(root_dir): + return [] + try: + root_mtime_ns = os.stat(root_dir).st_mtime_ns + except FileNotFoundError: + return [] + return DownloadList._get_all_cached_song_ids(int(root_mtime_ns)) def add_songs(self, song_ids: list = None) -> None: '''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲''' diff --git a/latest version/core/init.py b/latest version/core/init.py index affa6f1..86c3835 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -327,7 +327,8 @@ class FileChecker: def check_song_file(self) -> bool: '''检查song有关文件并初始化缓存''' f = self.check_folder(Config.SONG_FILE_FOLDER_PATH) - self.logger.info("Initialize song data...") + song_cache_path = os.path.join(os.path.dirname(Config.SQLITE_DATABASE_PATH) or '.', 'song_cache.db') + self.logger.info("Initialize song data..." if not os.path.isfile(song_cache_path) else "Reusing song data cache...") try: DownloadList.initialize_cache() if not Config.SONG_FILE_HASH_PRE_CALCULATE: From 707f881bdbc96b4cf9fa781e259b53aca1035d41 Mon Sep 17 00:00:00 2001 From: A-random-githuber <89432931+A-random-githuber@users.noreply.github.com> Date: Fri, 26 Dec 2025 00:27:29 +0700 Subject: [PATCH 2/4] Cache song --- main.py | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..6a1676b --- /dev/null +++ b/main.py @@ -0,0 +1,246 @@ +# encoding: utf-8 + +import json +import os +from importlib import import_module + +from core.config_manager import Config, ConfigManager + +if os.path.exists('config.py') or os.path.exists('config'): + # 导入用户自定义配置 + ConfigManager.load(import_module("config").Config) +else: + # Allow importing the config from a custom path given through an environment variable + configPath = os.environ.get("ARCAEA_JSON_CONFIG_PATH") + if configPath and os.path.exists(configPath): + with open(configPath, 'r') as file: + ConfigManager.load_dict(json.load(file)) + + +if Config.DEPLOY_MODE == 'gevent': + # 异步 + from gevent import monkey + monkey.patch_all() + + +import sys +from logging.config import dictConfig +from multiprocessing import Process, current_process, set_start_method +from traceback import format_exc + +from flask import Flask, make_response, request, send_from_directory + +import api +import server +import web.index +import web.login +# import webapi +from core.bundle import BundleDownload +from core.constant import Constant +from core.download import UserDownload +from core.error import ArcError, NoAccess, RateLimit +from core.init import FileChecker +from core.sql import Connect +from server.func import error_return + +app = Flask(__name__) + +if Config.USE_PROXY_FIX: + # 代理修复 + from werkzeug.middleware.proxy_fix import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) +if Config.USE_CORS: + # 服务端跨域 + from flask_cors import CORS + CORS(app, supports_credentials=True) + + +os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径 + + +app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY) +app.config['SESSION_TYPE'] = 'filesystem' +app.register_blueprint(web.login.bp) +app.register_blueprint(web.index.bp) +app.register_blueprint(api.bp) +list(map(app.register_blueprint, server.get_bps())) +# app.register_blueprint(webapi.bp) + + +@app.route('/') +def hello(): + return "Hello World!" + + +@app.route('/favicon.ico', methods=['GET']) # 图标 +def favicon(): + # Pixiv ID: 82374369 + # 我觉得这张图虽然并不是那么精细,但很有感觉,色彩的强烈对比下给人带来一种惊艳 + # 然后在压缩之下什么也看不清了:( + + return app.send_static_file('favicon.ico') + + +@app.route('/download/', methods=['GET']) # 下载 +def download(file_path): + with Connect(in_memory=True) as c: + try: + x = UserDownload(c) + x.token = request.args.get('t') + x.song_id, x.file_name = file_path.split('/', 1) + x.select_for_check() + if x.is_limited: + raise RateLimit( + f'User `{x.user.user_id}` has reached the download limit.', 903) + if not x.is_valid: + raise NoAccess('Expired token.') + x.download_hit() + if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: + # nginx X-Accel-Redirect + response = make_response() + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path + return response + return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) + except ArcError as e: + if Config.ALLOW_WARNING_LOG: + app.logger.warning(format_exc()) + return error_return(e) + return error_return() + + +@app.route('/bundle_download/', methods=['GET']) # 热更新下载 +def bundle_download(token: str): + with Connect(in_memory=True) as c_m: + try: + file_path = BundleDownload(c_m).get_path_by_token( + token, request.remote_addr) + if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: + # nginx X-Accel-Redirect + response = make_response() + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['X-Accel-Redirect'] = Config.BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX + file_path + return response + return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) + except ArcError as e: + if Config.ALLOW_WARNING_LOG: + app.logger.warning(format_exc()) + return error_return(e) + return error_return() + + +if Config.DEPLOY_MODE == 'waitress': + # 给waitress加个日志 + @app.after_request + def after_request(response): + app.logger.info( + f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}') + return response + +# @app.before_request +# def before_request(): +# print(request.path) +# print(request.headers) +# print(request.data) + + +def tcp_server_run(): + if Config.DEPLOY_MODE == 'gevent': + # 异步 gevent WSGI server + host_port = (Config.HOST, Config.PORT) + app.logger.info('Running gevent WSGI server... (%s:%s)' % host_port) + from gevent.pywsgi import WSGIServer + WSGIServer(host_port, app, log=app.logger).serve_forever() + elif Config.DEPLOY_MODE == 'waitress': + # waitress WSGI server + import logging + from waitress import serve # type: ignore + logger = logging.getLogger('waitress') + logger.setLevel(logging.INFO) + serve(app, host=Config.HOST, port=Config.PORT) + else: + if Config.SSL_CERT and Config.SSL_KEY: + app.run(Config.HOST, Config.PORT, ssl_context=( + Config.SSL_CERT, Config.SSL_KEY)) + else: + app.run(Config.HOST, Config.PORT) + + +def generate_log_file_dict(level: str, filename: str) -> dict: + return { + "class": "logging.handlers.RotatingFileHandler", + "maxBytes": 1024 * 1024, + "backupCount": 1, + "encoding": "utf-8", + "level": level, + "formatter": "default", + "filename": filename + } + + +def pre_main(): + log_dict = { + 'version': 1, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi', 'error_file'] + }, + 'handlers': { + 'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }, + "error_file": generate_log_file_dict('ERROR', f'{Config.LOG_FOLDER_PATH}/error.log') + }, + 'formatters': { + 'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' + } + } + } + if Config.ALLOW_INFO_LOG: + log_dict['root']['handlers'].append('info_file') + log_dict['handlers']['info_file'] = generate_log_file_dict( + 'INFO', f'{Config.LOG_FOLDER_PATH}/info.log') + if Config.ALLOW_WARNING_LOG: + log_dict['root']['handlers'].append('warning_file') + log_dict['handlers']['warning_file'] = generate_log_file_dict( + 'WARNING', f'{Config.LOG_FOLDER_PATH}/warning.log') + + dictConfig(log_dict) + + Connect.logger = app.logger + if not FileChecker(app.logger).check_before_run(): + app.logger.error('Some errors occurred. The server will not run.') + input('Press ENTER key to exit.') + sys.exit() + + +def main(): + if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS: + from linkplay_server import link_play + process = [Process(target=link_play, args=( + Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))] + [p.start() for p in process] + app.logger.info( + f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...") + app.logger.info( + f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...") + tcp_server_run() + [p.join() for p in process] + else: + tcp_server_run() + + +# must run for init +# this ensures avoiding duplicate init logs for some reason +if current_process().name == 'MainProcess': + pre_main() + +if __name__ == '__main__': + set_start_method("spawn") + main() + + +# Made By Lost 2020.9.11 From 73f28f38d3d3be1bbf97f3c63cd47f073c32d8f0 Mon Sep 17 00:00:00 2001 From: A-random-githuber <89432931+A-random-githuber@users.noreply.github.com> Date: Fri, 26 Dec 2025 00:33:28 +0700 Subject: [PATCH 3/4] Cache song --- latest version/main.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/latest version/main.py b/latest version/main.py index 05a7c42..6a1676b 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -1,5 +1,6 @@ # encoding: utf-8 +import json import os from importlib import import_module @@ -7,7 +8,14 @@ from core.config_manager import Config, ConfigManager if os.path.exists('config.py') or os.path.exists('config'): # 导入用户自定义配置 - ConfigManager.load(import_module('config').Config) + ConfigManager.load(import_module("config").Config) +else: + # Allow importing the config from a custom path given through an environment variable + configPath = os.environ.get("ARCAEA_JSON_CONFIG_PATH") + if configPath and os.path.exists(configPath): + with open(configPath, 'r') as file: + ConfigManager.load_dict(json.load(file)) + if Config.DEPLOY_MODE == 'gevent': # 异步 @@ -17,7 +25,7 @@ if Config.DEPLOY_MODE == 'gevent': import sys from logging.config import dictConfig -from multiprocessing import Process, set_start_method +from multiprocessing import Process, current_process, set_start_method from traceback import format_exc from flask import Flask, make_response, request, send_from_directory @@ -183,7 +191,7 @@ def pre_main(): 'stream': 'ext://flask.logging.wsgi_errors_stream', 'formatter': 'default' }, - "error_file": generate_log_file_dict('ERROR', './log/error.log') + "error_file": generate_log_file_dict('ERROR', f'{Config.LOG_FOLDER_PATH}/error.log') }, 'formatters': { 'default': { @@ -194,11 +202,11 @@ def pre_main(): if Config.ALLOW_INFO_LOG: log_dict['root']['handlers'].append('info_file') log_dict['handlers']['info_file'] = generate_log_file_dict( - 'INFO', './log/info.log') + 'INFO', f'{Config.LOG_FOLDER_PATH}/info.log') if Config.ALLOW_WARNING_LOG: log_dict['root']['handlers'].append('warning_file') log_dict['handlers']['warning_file'] = generate_log_file_dict( - 'WARNING', './log/warning.log') + 'WARNING', f'{Config.LOG_FOLDER_PATH}/warning.log') dictConfig(log_dict) @@ -226,7 +234,9 @@ def main(): # must run for init -pre_main() +# this ensures avoiding duplicate init logs for some reason +if current_process().name == 'MainProcess': + pre_main() if __name__ == '__main__': set_start_method("spawn") From 2cc69a11007c852039975ae793fae928d0e4c715 Mon Sep 17 00:00:00 2001 From: A-random-githuber <89432931+A-random-githuber@users.noreply.github.com> Date: Fri, 26 Dec 2025 00:35:34 +0700 Subject: [PATCH 4/4] oop --- main.py | 246 -------------------------------------------------------- 1 file changed, 246 deletions(-) delete mode 100644 main.py diff --git a/main.py b/main.py deleted file mode 100644 index 6a1676b..0000000 --- a/main.py +++ /dev/null @@ -1,246 +0,0 @@ -# encoding: utf-8 - -import json -import os -from importlib import import_module - -from core.config_manager import Config, ConfigManager - -if os.path.exists('config.py') or os.path.exists('config'): - # 导入用户自定义配置 - ConfigManager.load(import_module("config").Config) -else: - # Allow importing the config from a custom path given through an environment variable - configPath = os.environ.get("ARCAEA_JSON_CONFIG_PATH") - if configPath and os.path.exists(configPath): - with open(configPath, 'r') as file: - ConfigManager.load_dict(json.load(file)) - - -if Config.DEPLOY_MODE == 'gevent': - # 异步 - from gevent import monkey - monkey.patch_all() - - -import sys -from logging.config import dictConfig -from multiprocessing import Process, current_process, set_start_method -from traceback import format_exc - -from flask import Flask, make_response, request, send_from_directory - -import api -import server -import web.index -import web.login -# import webapi -from core.bundle import BundleDownload -from core.constant import Constant -from core.download import UserDownload -from core.error import ArcError, NoAccess, RateLimit -from core.init import FileChecker -from core.sql import Connect -from server.func import error_return - -app = Flask(__name__) - -if Config.USE_PROXY_FIX: - # 代理修复 - from werkzeug.middleware.proxy_fix import ProxyFix - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) -if Config.USE_CORS: - # 服务端跨域 - from flask_cors import CORS - CORS(app, supports_credentials=True) - - -os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径 - - -app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY) -app.config['SESSION_TYPE'] = 'filesystem' -app.register_blueprint(web.login.bp) -app.register_blueprint(web.index.bp) -app.register_blueprint(api.bp) -list(map(app.register_blueprint, server.get_bps())) -# app.register_blueprint(webapi.bp) - - -@app.route('/') -def hello(): - return "Hello World!" - - -@app.route('/favicon.ico', methods=['GET']) # 图标 -def favicon(): - # Pixiv ID: 82374369 - # 我觉得这张图虽然并不是那么精细,但很有感觉,色彩的强烈对比下给人带来一种惊艳 - # 然后在压缩之下什么也看不清了:( - - return app.send_static_file('favicon.ico') - - -@app.route('/download/', methods=['GET']) # 下载 -def download(file_path): - with Connect(in_memory=True) as c: - try: - x = UserDownload(c) - x.token = request.args.get('t') - x.song_id, x.file_name = file_path.split('/', 1) - x.select_for_check() - if x.is_limited: - raise RateLimit( - f'User `{x.user.user_id}` has reached the download limit.', 903) - if not x.is_valid: - raise NoAccess('Expired token.') - x.download_hit() - if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: - # nginx X-Accel-Redirect - response = make_response() - response.headers['Content-Type'] = 'application/octet-stream' - response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path - return response - return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) - except ArcError as e: - if Config.ALLOW_WARNING_LOG: - app.logger.warning(format_exc()) - return error_return(e) - return error_return() - - -@app.route('/bundle_download/', methods=['GET']) # 热更新下载 -def bundle_download(token: str): - with Connect(in_memory=True) as c_m: - try: - file_path = BundleDownload(c_m).get_path_by_token( - token, request.remote_addr) - if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: - # nginx X-Accel-Redirect - response = make_response() - response.headers['Content-Type'] = 'application/octet-stream' - response.headers['X-Accel-Redirect'] = Config.BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX + file_path - return response - return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) - except ArcError as e: - if Config.ALLOW_WARNING_LOG: - app.logger.warning(format_exc()) - return error_return(e) - return error_return() - - -if Config.DEPLOY_MODE == 'waitress': - # 给waitress加个日志 - @app.after_request - def after_request(response): - app.logger.info( - f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}') - return response - -# @app.before_request -# def before_request(): -# print(request.path) -# print(request.headers) -# print(request.data) - - -def tcp_server_run(): - if Config.DEPLOY_MODE == 'gevent': - # 异步 gevent WSGI server - host_port = (Config.HOST, Config.PORT) - app.logger.info('Running gevent WSGI server... (%s:%s)' % host_port) - from gevent.pywsgi import WSGIServer - WSGIServer(host_port, app, log=app.logger).serve_forever() - elif Config.DEPLOY_MODE == 'waitress': - # waitress WSGI server - import logging - from waitress import serve # type: ignore - logger = logging.getLogger('waitress') - logger.setLevel(logging.INFO) - serve(app, host=Config.HOST, port=Config.PORT) - else: - if Config.SSL_CERT and Config.SSL_KEY: - app.run(Config.HOST, Config.PORT, ssl_context=( - Config.SSL_CERT, Config.SSL_KEY)) - else: - app.run(Config.HOST, Config.PORT) - - -def generate_log_file_dict(level: str, filename: str) -> dict: - return { - "class": "logging.handlers.RotatingFileHandler", - "maxBytes": 1024 * 1024, - "backupCount": 1, - "encoding": "utf-8", - "level": level, - "formatter": "default", - "filename": filename - } - - -def pre_main(): - log_dict = { - 'version': 1, - 'root': { - 'level': 'INFO', - 'handlers': ['wsgi', 'error_file'] - }, - 'handlers': { - 'wsgi': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://flask.logging.wsgi_errors_stream', - 'formatter': 'default' - }, - "error_file": generate_log_file_dict('ERROR', f'{Config.LOG_FOLDER_PATH}/error.log') - }, - 'formatters': { - 'default': { - 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' - } - } - } - if Config.ALLOW_INFO_LOG: - log_dict['root']['handlers'].append('info_file') - log_dict['handlers']['info_file'] = generate_log_file_dict( - 'INFO', f'{Config.LOG_FOLDER_PATH}/info.log') - if Config.ALLOW_WARNING_LOG: - log_dict['root']['handlers'].append('warning_file') - log_dict['handlers']['warning_file'] = generate_log_file_dict( - 'WARNING', f'{Config.LOG_FOLDER_PATH}/warning.log') - - dictConfig(log_dict) - - Connect.logger = app.logger - if not FileChecker(app.logger).check_before_run(): - app.logger.error('Some errors occurred. The server will not run.') - input('Press ENTER key to exit.') - sys.exit() - - -def main(): - if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS: - from linkplay_server import link_play - process = [Process(target=link_play, args=( - Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))] - [p.start() for p in process] - app.logger.info( - f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...") - app.logger.info( - f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...") - tcp_server_run() - [p.join() for p in process] - else: - tcp_server_run() - - -# must run for init -# this ensures avoiding duplicate init logs for some reason -if current_process().name == 'MainProcess': - pre_main() - -if __name__ == '__main__': - set_start_method("spawn") - main() - - -# Made By Lost 2020.9.11