# -*- coding: utf-8 -*- """服务类与进程级服务实例。 """ import logging import re from pathlib import Path from typing import Any from urllib.parse import parse_qs, urlsplit from core.config import ( Settings, ) from core.utils import expand_shortlink as _expand_shortlink from music_api import ( NeteaseAPI, load_cookies, ) from music_downloader import ( MusicDownloader, ) # 向后兼容别名:既有代码/文档沿用 APIConfig 名称 APIConfig = Settings class APIResponse: """API响应工具类""" @staticmethod def success(data: Any = None, message: str = 'success', status_code: int = 200) -> tuple[dict[str, Any], int]: """成功响应""" response = { 'status': status_code, 'success': True, 'message': message } if data is not None: response['data'] = data return response, status_code @staticmethod def error(message: str, status_code: int = 400, error_code: str | None = None) -> tuple[dict[str, Any], int]: """错误响应""" response = { 'status': status_code, 'success': False, 'message': message } if error_code: response['error_code'] = error_code return response, status_code 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 _extract_music_id(self, id_or_url: str) -> str: """提取音乐ID(正规 URL 解析替代子串/删除非数字启发式) - 短链仅当整串就是一个 163cn.tv 短链 URL 时才请求展开(防 SSRF) - music.163.com 链接用 parse_qs 取 id 参数(参数顺序无关) """ raw = str(id_or_url or '').strip() try: # 短链展开:仅接受"整个输入就是 163cn.tv 链接"(子串包含不算) if re.match(r'^https?://(www\.)?163cn\.tv/\S+$', raw, re.IGNORECASE): raw = _expand_shortlink(raw) # 网易云链接:正规解析 query 取 id(不再 find('id=') 误吃 userid= 尾部) if 'music.163.com' in raw: try: q = parse_qs(urlsplit(raw).query) for key in ('id', 'songid'): if q.get(key): return str(q[key][0]).strip() except Exception: pass # 兜底:老格式 #/song?id=xxx 的片段部分 m = re.search(r'[?&#]id=(\d+)', raw) if m: return m.group(1) # 纯数字/普通文本直接返回 return raw except Exception as e: self.logger.error(f"提取音乐ID失败: {e}") return str(id_or_url or '').strip() 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)