This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/services/api_service.py
T
snowy-yang 0569b50737 refactor: 清理 13 处生产死代码
为什么:用 AST 级引用分析(而非文本 grep——同名符号会互相掩盖)确认这些
符号全项目零引用。文本 grep 查不出的原因是它们都有「活着的同名兄弟」:

services/downloads.py(共 6 个,93 行)
  _parse_cookie_str / _global_music_u / _vip_name / _detect_vip /
  _admin_client / _user_client
  —— 活的是 core.media.parse_cookie_str、web/routers/music.py、account.py 的版本
  随之失效的 import:USER_COOKIE_FILE / GLOBAL_MUSIC_U_CACHE / VIP_PROBE_LOCK /
  account_info / detect_vip_by_probe / QRLoginClient

services/api_service.py
  APIResponse 类(29 行)—— web/response.py 才是活的响应封装

services/_shared.py
  _rate_ok —— 逐字复制 web.ratelimit.rate_ok

core/utils.py
  safe_int / safe_console_print —— 活的是 web/request_utils.safe_int

core/repo.py
  all_settings / get_user_by_name —— auth_system.get_user_by_name 才是活的

core/db.py / web/auth_state.py / web/request_utils.py
  execute_many / _safe_register_credits + secrets_rand / dedup_ids

结果:生产代码死代码候选 13 → 0(余下 118 个全是 pytest 测试函数,属正常)。
2026-09-26 18:54:11 +08:00

151 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""服务类与进程级服务实例。
"""
import logging
import re
from pathlib import Path
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 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)