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 测试函数,属正常)。
This commit is contained in:
snowy-yang committed 2026-09-26 18:54:11 +08:00
1 parent 445c16c329
commit 0569b50737
8 files changed
+10 -217

No files matched your search

+1 -13
View File
@@ -14,7 +14,7 @@
import asyncio
import os
from collections.abc import AsyncGenerator, Iterable, Sequence
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
@@ -126,18 +126,6 @@ async def execute(sql: str, params: Sequence = ()) -> int:
await conn.close()
async def execute_many(sql: str, seq: Iterable[Sequence]) -> int:
"""批量写,返回受影响行数。"""
async with _write_lock():
conn = await connect()
try:
cur = await conn.executemany(sql, seq)
await conn.commit()
return cur.rowcount
finally:
await conn.close()
@asynccontextmanager
async def write_transaction() -> AsyncGenerator[aiosqlite.Connection, None]:
"""写事务上下文:多条写语句原子提交(串行锁内)。
-10
View File
@@ -28,11 +28,6 @@ async def set_setting(key: str, value: str) -> None:
)
async def all_settings() -> dict[str, str]:
rows = await db.fetch_all('SELECT key, value FROM settings')
return {r['key']: r['value'] for r in rows}
# ---------- announcements ----------
async def list_announcements(limit: int = 20) -> list[dict[str, Any]]:
@@ -49,11 +44,6 @@ async def count_users(role: str | None = None) -> int:
return int(await db.fetch_val('SELECT COUNT(*) FROM users', (), 0))
async def get_user_by_name(username: str) -> dict[str, Any] | None:
row = await db.fetch_one('SELECT * FROM users WHERE username = ?', (username,))
return dict(row) if row is not None else None
async def get_user_by_id(uid: int) -> dict[str, Any] | None:
row = await db.fetch_one('SELECT * FROM users WHERE id = ?', (uid,))
return dict(row) if row is not None else None
+1 -22
View File
@@ -1,34 +1,13 @@
# -*- coding: utf-8 -*-
"""纯函数工具(框架无关,无 Web 框架依赖)。
安全整数解析、控制台输出兜底、SRT 时间轴、
LRC→SRT 转换、字段参数解析、163cn.tv 短链展开(含私有缓存)。
SRT 时间轴、LRC→SRT 转换、字段参数解析、163cn.tv 短链展开(含私有缓存)。
"""
import re
import sys
import threading
def safe_int(value, default: int) -> int:
"""用户输入的安全整数解析:垃圾值回退默认值(而非 500)"""
try:
return int(value)
except (TypeError, ValueError):
return default
def safe_console_print(text: str) -> None:
"""控制台输出兜底:stdout 被重定向到文件且系统编码非 UTF-8 时
(典型:NSSM 服务日志 + 中文 Windows GBK 控制台),emoji 等字符会触发
UnicodeEncodeError 中断启动流程——实测崩溃循环。此处降级为可编码字符。"""
try:
print(text)
except UnicodeEncodeError:
enc = getattr(sys.stdout, 'encoding', None) or 'ascii'
print(text.encode(enc, errors='replace').decode(enc, errors='replace'))
def srt_time(t: float) -> str:
"""秒 → SRT 时间格式 HH:MM:SS,mmm"""
h = int(t // 3600)
+4 -21
View File
@@ -2,7 +2,6 @@
"""服务层共享状态与低层工具(限流桶、请求统计、命名与常量)。
"""
import collections
import re
import time
from typing import Any
@@ -19,25 +18,8 @@ from music_downloader import (
CountingSemaphore,
)
def _rate_ok(key: str, limit: int, window: int) -> bool:
"""滑动窗口限流:True=放行,False=超限"""
now = time.time()
with _RATE_LOCK:
dq = _RATE_BUCKETS.get(key)
if dq is None:
# 桶数量防膨胀:超限时清理不活跃桶
if len(_RATE_BUCKETS) > 8000:
for k in [k for k, v in _RATE_BUCKETS.items() if not v or now - v[-1] > window]:
_RATE_BUCKETS.pop(k, None)
dq = collections.deque()
_RATE_BUCKETS[key] = dq
while dq and now - dq[0] > window:
dq.popleft()
if len(dq) >= limit:
return False
dq.append(now)
return True
# 限流实现只有一处:web.ratelimit.rate_ok(同读 core.state 的桶,
# 此处不再保留同名副本——曾有两份逐字相同的滑动窗口实现)
# 旧版管理员口令已废除(2026-09-15):管理员权限仅认站点管理员会话(admin 登录),
@@ -76,7 +58,8 @@ def _sweep_rate_buckets() -> None:
_RATE_BUCKETS.pop(k, None)
# _safe_int / _safe_console_print 已迁出至 core.utils(此处 import 同名共享)
# 参数安全转换只有一处:web.request_utils.safe_int
# (原 core.utils.safe_int / safe_console_print 与 _shared 的副本均已删除——三份实现零调用)
# ============ 接口请求统计 ============
-29
View File
@@ -5,7 +5,6 @@
import logging
import re
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlsplit
from core.config import (
@@ -24,34 +23,6 @@ from music_downloader import (
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服务类"""
+4 -89
View File
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
"""单曲下载、账号探测与任务交付。
"""单曲下载任务 worker:下载 → 打包 → 交付 → 结算。
账号探测(VIP/音质)与二维码登录客户端均已迁出(前者只剩 web.routers.account
的异步实现,后者只剩 web.routers.admin 自己的客户端),本模块只负责单曲任务。
"""
import os
@@ -10,21 +13,11 @@ import zipfile
from pathlib import Path
import auth_system
from core.config import (
USER_COOKIE_FILE,
)
from core.state import (
DOWNLOAD_TASKS,
DOWNLOAD_TASKS_LOCK,
)
from core.state import GLOBAL_MUSIC_U_CACHE as _GLOBAL_MUSIC_U_CACHE
from core.state import VIP_PROBE_LOCK as _VIP_PROBE_LOCK
from core.utils import format_lyric as _format_lyric
from music_api import (
account_info,
detect_vip_by_probe,
)
from music_api.qr_client import QRLoginClient
from music_downloader import (
DownloadCanceled,
DownloadException,
@@ -43,84 +36,6 @@ from .api_service import (
)
def _parse_cookie_str(raw) -> dict[str, str]:
"""将 'k=v;k2=v2' 格式的 Cookie 字符串解析为字典"""
cookies: dict[str, str] = {}
if not raw:
return cookies
for part in str(raw).split(';'):
if '=' in part:
k, v = part.strip().split('=', 1)
if k:
cookies[k] = v
return cookies
# _GLOBAL_MUSIC_U_CACHE 已迁出至 core.state(此处 import 同名共享)
def _global_music_u() -> str:
"""全局账号 MUSIC_U(5 分钟缓存;后台重新扫码后自动跟随新值)"""
now = time.time()
if _GLOBAL_MUSIC_U_CACHE['v'] is None or now - _GLOBAL_MUSIC_U_CACHE['ts'] > 300:
try:
ck = api_service._get_cookies()
_GLOBAL_MUSIC_U_CACHE['v'] = (ck.get('MUSIC_U') or '').strip()
except Exception:
_GLOBAL_MUSIC_U_CACHE['v'] = ''
_GLOBAL_MUSIC_U_CACHE['ts'] = now
# 契约返回 str:缓存未命中且读取失败时 v 会是 None,统一收敛为空串
return _GLOBAL_MUSIC_U_CACHE['v'] or ''
def _vip_name(vip_type: int) -> str:
"""根据 vipType 返回会员类型名称(兼容新旧取值)"""
return {
0: '普通用户',
1: '音乐包',
11: '黑胶VIP',
100: '黑胶VIP',
10: '黑胶SVIP',
110: '黑胶SVIP'
}.get(vip_type, '普通用户')
def _detect_vip(cookies: dict[str, str]) -> tuple[int, str, list[str]]:
"""检测账号会员类型与可解析音质:
优先账号接口(vipType 精确),并始终用音质探测接口实测可解析列表。
探测单飞锁(有界等待 0.5s)——上游黑洞时并发请求不再各自串行
8 级探测(最坏 16 分钟线程滞留),等锁超时者直接用账号接口给降级结果"""
if not _VIP_PROBE_LOCK.acquire(timeout=0.5):
info = account_info(cookies)
if info:
return info.get('vipType', 0), 'account', []
return 0, 'probe', []
try:
probe = detect_vip_by_probe(cookies)
levels_ok = probe.get('levels_ok', [])
info = account_info(cookies)
if info:
return info.get('vipType', 0), 'account', levels_ok
return probe.get('vip_type', 0), 'probe', levels_ok
finally:
_VIP_PROBE_LOCK.release()
# ============ 管理员设置(二维码登录 / Cookie 管理) ============
def _admin_client() -> QRLoginClient:
"""创建二维码登录客户端(基于 cookie.txt)"""
return QRLoginClient()
# ============ 个人歌单(游客扫码登录自己的账号) ============
def _user_client() -> QRLoginClient:
"""创建个人用户二维码登录客户端(后不再落盘 user_cookie.txt,
USER_COOKIE_FILE 仅作 QRLoginClient 构造参数遗留)"""
return QRLoginClient(USER_COOKIE_FILE)
def _task_delivery_copy(task_id: str, src: Path, filename: str) -> str:
"""把共享缓存文件复制/硬链接为任务专属交付文件(任务成果与共享缓存分离,
任何任务的清理都不会再删掉别的任务的成果)"""
-17
View File
@@ -7,8 +7,6 @@
import secrets
import time
import auth_system as auth
# ---- 登录失败锁定(按用户名维度,10 分钟窗口内 >=5 次临时锁定)----
_LOGIN_FAILS: dict = {}
_LOGIN_FAIL_SWEEP_TS = 0.0
@@ -119,16 +117,6 @@ def _login_fail_locked_log_once(username: str) -> bool:
return True
def _safe_register_credits() -> int:
"""注册赠送次数(settings.register_credits,默认 3)。
防呆:损坏回退默认;<0 一律按 0——设为 -1 会经 create_user 造出"不限次"新号。"""
try:
credits = int(auth.get_setting('register_credits', str(auth.DEFAULT_REGISTER_CREDITS)) or 0)
except (TypeError, ValueError):
return auth.DEFAULT_REGISTER_CREDITS
return max(0, credits)
_SEND_CODE_IP = {}
@@ -165,8 +153,3 @@ def re_username_ok(username: str) -> bool:
return bool(_re.fullmatch(r'[A-Za-z0-9_\u4e00-\u9fa5]{3,20}', username)) and \
not _re.match(r'^_+$', username) and not username.startswith('_') and not username.endswith('_') \
and username.lower() not in _RESERVED_USERNAMES
def secrets_rand() -> int:
import secrets as _s
return _s.randbelow(900000) + 100000
-16
View File
@@ -65,19 +65,3 @@ def cookies() -> dict[str, str]:
"""全局账号 Cookie(管理员 cookie.txt)。"""
import music_api
return music_api.load_cookies()
def dedup_ids(raw_ids: Any, transform=extract_id) -> list[str]:
"""ID 列表去重(保持首次出现顺序):与 Flask 侧「提取→去重」计费口径一致。"""
if isinstance(raw_ids, str):
raw_list = raw_ids.split(',')
else:
raw_list = [str(x) for x in (raw_ids or [])]
out: list[str] = []
seen: set[str] = set()
for x in raw_list:
s = str(transform(x) or '').strip()
if s and s not in seen:
seen.add(s)
out.append(s)
return out