Archived
「同步转异步」的残留清理: 1) auth_system/geo.query_ip_province 原经 core.sync_http 同步门面(在后台 事件循环桥接)查询归属地,但其唯一调用方 record_login_location 的入口 本来就是 async 的登录链路(web/routers/auth 里还套了一层 to_thread)。 改为直接 await core.http_client——桥接纯属多余,顺带把 record_login_location 转 async,落库仍走同步 sqlite 但经 asyncio.to_thread 卸载,避免 sqlite busy 等待(最长 15s)冻结事件循环。 调用方两处(普通用户/管理员登录)去掉 to_thread 包装,直接 await。 2) MusicAPIService._extract_music_id 是死代码:ID 归一化早已迁到 web.request_utils(注释里明说是它的迁移口径),全仓零调用。它也是 core.utils.expand_shortlink 的唯一调用方——一并删除,core.utils 由此 彻底不再 import core.sync_http。 门禁:ruff 全过 / pyright 0-0 / pytest 387 项全绿(0 失败)。
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""服务类与进程级服务实例。
|
||
"""
|
||
|
||
import logging
|
||
from pathlib import Path
|
||
|
||
from core.config import (
|
||
Settings,
|
||
)
|
||
from music_api import (
|
||
NeteaseAPI,
|
||
load_cookies,
|
||
)
|
||
from music_downloader import (
|
||
MusicDownloader,
|
||
)
|
||
|
||
# 向后兼容别名:既有代码/文档沿用 APIConfig 名称
|
||
APIConfig = Settings
|
||
|
||
|
||
class MusicAPIService:
|
||
"""音乐API服务类"""
|
||
|
||
def __init__(self, config: APIConfig):
|
||
self.config = config
|
||
self.logger = self._setup_logger()
|
||
self.netease_api = NeteaseAPI()
|
||
# 下载目录传给下载器——原实现下载器固定 downloads,
|
||
# 服务配置指向自定义目录时音乐落盘位置与配置分离
|
||
self.downloader = MusicDownloader(download_dir=config.downloads_dir)
|
||
|
||
# 创建下载目录
|
||
self.downloads_path = Path(config.downloads_dir)
|
||
self.downloads_path.mkdir(exist_ok=True)
|
||
|
||
self.logger.info(f"音乐API服务初始化完成,下载目录: {self.downloads_path.absolute()}")
|
||
|
||
def _setup_logger(self) -> logging.Logger:
|
||
"""设置日志记录器"""
|
||
logger = logging.getLogger('music_api')
|
||
logger.setLevel(getattr(logging, self.config.log_level.upper()))
|
||
|
||
if not logger.handlers:
|
||
# 控制台处理器
|
||
console_handler = logging.StreamHandler()
|
||
console_formatter = logging.Formatter(
|
||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
console_handler.setFormatter(console_formatter)
|
||
logger.addHandler(console_handler)
|
||
|
||
# 文件处理器(轮转:单文件 5MB,保留 3 份,防止长期运行无限膨胀)
|
||
try:
|
||
from logging.handlers import RotatingFileHandler
|
||
file_handler = RotatingFileHandler('music_api.log', maxBytes=5 * 1024 * 1024,
|
||
backupCount=3, encoding='utf-8')
|
||
file_formatter = logging.Formatter(
|
||
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
|
||
)
|
||
file_handler.setFormatter(file_formatter)
|
||
logger.addHandler(file_handler)
|
||
except Exception as e:
|
||
logger.warning(f"无法创建日志文件: {e}")
|
||
|
||
return logger
|
||
|
||
def _get_cookies(self) -> dict[str, str]:
|
||
"""直接从 cookie.txt 读取并解析为 dict"""
|
||
try:
|
||
cookies = load_cookies()
|
||
if not cookies:
|
||
self.logger.warning("cookie.txt 为空或不存在,部分接口(VIP 音质)将不可用")
|
||
return cookies
|
||
except Exception as e:
|
||
self.logger.error(f"读取 cookie.txt 异常: {e}")
|
||
return {}
|
||
|
||
def _format_file_size(self, size_bytes) -> str:
|
||
"""格式化文件大小(上游 size 可能为 null/垃圾值:容错返回"大小未知"而非 500)"""
|
||
try:
|
||
size = float(size_bytes or 0)
|
||
except (TypeError, ValueError):
|
||
return "大小未知"
|
||
if size <= 0:
|
||
return "0B"
|
||
|
||
units = ["B", "KB", "MB", "GB", "TB"]
|
||
unit_index = 0
|
||
|
||
while size >= 1024.0 and unit_index < len(units) - 1:
|
||
size /= 1024.0
|
||
unit_index += 1
|
||
|
||
return f"{size:.2f}{units[unit_index]}"
|
||
|
||
def _get_quality_display_name(self, quality: str) -> str:
|
||
"""获取音质显示名称"""
|
||
quality_names = {
|
||
'standard': "标准音质",
|
||
'exhigh': "极高音质",
|
||
'lossless': "无损音质",
|
||
'hires': "Hi-Res音质",
|
||
'sky': "沉浸环绕声",
|
||
'jyeffect': "高清环绕声",
|
||
'jymaster': "超清母带",
|
||
'dolby': "杜比全景声"
|
||
}
|
||
return quality_names.get(quality, f"未知音质({quality})")
|
||
|
||
|
||
# 服务实例(框架无关)
|
||
config = APIConfig()
|
||
|
||
api_service = MusicAPIService(config)
|