Archived
为什么:用 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 测试函数,属正常)。
151 lines
7.1 KiB
Python
151 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""服务层共享状态与低层工具(限流桶、请求统计、命名与常量)。
|
||
"""
|
||
|
||
import re
|
||
import time
|
||
from typing import Any
|
||
|
||
from core.config import (
|
||
BATCH_ZIP_CONCURRENCY,
|
||
)
|
||
from core.state import RATE_BUCKETS as _RATE_BUCKETS
|
||
from core.state import RATE_LOCK as _RATE_LOCK
|
||
from core.state import (
|
||
REQ_LOG,
|
||
)
|
||
from music_downloader import (
|
||
CountingSemaphore,
|
||
)
|
||
|
||
# 限流实现只有一处:web.ratelimit.rate_ok(同读 core.state 的桶,
|
||
# 此处不再保留同名副本——曾有两份逐字相同的滑动窗口实现)
|
||
|
||
|
||
# 旧版管理员口令已废除(2026-09-15):管理员权限仅认站点管理员会话(admin 登录),
|
||
# 见 _verify_admin_password。管理后台(/admin/site/*)与旧版二维码接口(/admin/qr/*)
|
||
# 均通过该会话鉴权。
|
||
|
||
# 全局批量打包并发信号量:限制同时构建的批量 ZIP 数量。
|
||
# 大量用户同时批量下载时,超出的请求排队等待,避免磁盘/内存/CPU 被同时占满。
|
||
# (下载本身的并发已由下载器内的全局信号量控制)
|
||
# 使用带计数的信号量,供前端实时展示排队人数与预计等待时间。
|
||
# 注:信号量依赖下载器的 CountingSemaphore,属服务层对象,随下载服务搬迁,
|
||
# 暂留 main(并发上限 3 见 core.config.BATCH_ZIP_CONCURRENCY)。
|
||
BATCH_ZIP_SEM = CountingSemaphore(BATCH_ZIP_CONCURRENCY)
|
||
|
||
|
||
_MONITOR_ERR_TS = {'ts': 0.0, 'n': 0} # 监控线程异常限频记录(持续异常不静默也不刷屏)
|
||
|
||
# 监控线程周期兜底的三个节流字典——补丁时定义丢失
|
||
# (只剩使用),NameError 被外层 except 吞成 WARNING,孤儿对账/账本清理/tasks
|
||
# 补扫自那以后从未执行(music_api.log 实证 2064 条 WARNING)。红转绿:重启后
|
||
# 日志零 NameError 且对账真实运行。
|
||
_LEDGER_LAST_RECOVER = {'ts': 0.0} # 上次孤儿账本对账时间(每 60s 一次)
|
||
|
||
_LEDGER_LAST_CLEANUP = {'ts': 0.0} # 上次 settled 账本 90 天清理时间(每 24h 一次)
|
||
|
||
_TASKS_SWEEP_LAST = {'ts': 0.0} # 上次 tasks/ 孤儿目录补扫时间(每小时一次)
|
||
|
||
|
||
def _sweep_rate_buckets() -> None:
|
||
"""限流桶周期清扫:把超过 10 分钟无新请求的桶移出,防止低流量长期运行下
|
||
缓慢累积(原实现只在"新建第 8001 个桶"时才触发清理,时机太晚)"""
|
||
now = time.time()
|
||
with _RATE_LOCK:
|
||
stale = [k for k, v in _RATE_BUCKETS.items() if not v or now - v[-1] > 600]
|
||
for k in stale:
|
||
_RATE_BUCKETS.pop(k, None)
|
||
|
||
|
||
# 参数安全转换只有一处:web.request_utils.safe_int
|
||
# (原 core.utils.safe_int / safe_console_print 与 _shared 的副本均已删除——三份实现零调用)
|
||
|
||
|
||
# ============ 接口请求统计 ============
|
||
# REQUEST_STATS / _STATS_LOCK / REQ_LOG 已迁出至 core.state(此处 import 同名共享)
|
||
|
||
# 只统计真实业务接口(排除首页/健康检查/统计/账号探测等内部调用)
|
||
# 兼容别名(/Search、/Song_V1、/Playlist、/Album、/Download)一并纳入统计与限流,
|
||
# 防止用大写别名绕过业务限流
|
||
COUNTED_PATHS = {
|
||
'/song', '/search', '/playlist', '/album', '/toplist', '/artist_songs',
|
||
'/comments', '/download', '/download/batch_zip',
|
||
'/podcast/parse', '/podcast/programs', '/podcast/search', '/podcast/hot',
|
||
'/podcast/download', '/podcast/download/batch_zip',
|
||
'/user/playlists', '/user/playlist', '/user/status', '/user/logout',
|
||
'/user/qr/create', '/user/qr/check',
|
||
# 账号探测与个人操作端点此前漏在限流名单外——
|
||
# /account/vip 每请求最多 9 次上游探测、/user/comment 与 /user/collect 携
|
||
# 任意 cookie 触发上游 weapi、/auth/me 可匿名无限刷,全部纳入免费 30/分桶
|
||
'/account/vip', '/user/collect', '/user/comment', '/auth/me',
|
||
'/admin/qr/create', '/admin/qr/check', '/admin/cookie/status',
|
||
'/admin/cookie/delete', '/admin/logs',
|
||
'/admin/site/play_key/get', '/admin/site/play_key/rotate',
|
||
'/global/playlists', '/global/playlist', '/api/playlist/play',
|
||
'/song_v1',
|
||
}
|
||
|
||
_COUNTED_LOWER = {p.lower() for p in COUNTED_PATHS}
|
||
|
||
|
||
ENDPOINT_NAMES = {
|
||
'/song': '歌曲信息', '/search': '搜索', '/playlist': '歌单详情',
|
||
'/album': '专辑详情', '/toplist': '榜单列表', '/artist_songs': '歌手歌曲',
|
||
'/comments': '评论', '/download': '音乐下载', '/download/batch_zip': '批量ZIP',
|
||
'/health': '健康检查', '/api/info': '服务信息', '/api/topen': '请求统计',
|
||
'/account/vip': '账号VIP信息',
|
||
'/podcast/parse': '播客解析', '/podcast/programs': '播客单集列表',
|
||
'/podcast/search': '播客搜索', '/podcast/hot': '热门播客',
|
||
'/podcast/download': '播客单集下载', '/podcast/download/batch_zip': '播客整台打包',
|
||
'/user/playlists': '个人歌单', '/user/playlist': '个人歌单详情',
|
||
'/user/status': '个人登录状态', '/user/logout': '个人退出',
|
||
'/user/qr/create': '个人二维码', '/user/qr/check': '个人登录轮询',
|
||
'/admin/qr/create': '管理员二维码', '/admin/qr/check': '管理员登录轮询',
|
||
'/admin/cookie/status': 'Cookie状态', '/admin/cookie/delete': 'Cookie管理',
|
||
'/admin/logs': '操作记录',
|
||
'/global/playlists': '全局账号歌单', '/global/playlist': '全局歌单详情',
|
||
'/api/playlist/play': '公开歌单播放API',
|
||
}
|
||
|
||
|
||
# 用户解析/下载操作记录(IP)——REQ_LOG 已迁出至 core.state(此处 import 同名共享)
|
||
|
||
|
||
def _record_log(action: str, ip: str, song: str) -> None:
|
||
"""记录用户操作(解析/下载)。歌名截断(来自上游/用户输入,不设上限会撑内存与后台页)"""
|
||
REQ_LOG.append({
|
||
'action': action,
|
||
'ip': ip,
|
||
'song': str(song or '')[:100],
|
||
'time': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||
})
|
||
if len(REQ_LOG) > 2000:
|
||
del REQ_LOG[:len(REQ_LOG) - 2000]
|
||
|
||
|
||
def _build_download_base(filename_format: str, music_info: dict[str, Any]) -> str:
|
||
"""按文件名格式生成下载基础名(不含扩展名)"""
|
||
name = music_info.get('name', '未知歌曲')
|
||
artist = music_info.get('artist_string', '') or '未知歌手'
|
||
|
||
def _safe(s: str) -> str:
|
||
# 交付文件名必须剔除控制字符——歌名含 \r\n 时交付头的
|
||
# Content-Disposition 会抛 ValueError,任务完成已扣费但领取必 500
|
||
# (当年只修了下载器磁盘名这条同型路径,交付名漏网)
|
||
s = re.sub(r'[\x00-\x1f\x7f]', '', s)
|
||
return ''.join(c for c in s if c not in r'<>:"/\|?*').strip() or 'unknown'
|
||
|
||
if filename_format == 'song':
|
||
return _safe(name)
|
||
if filename_format == 'song_artist':
|
||
return _safe(f"{name} - {artist}")
|
||
return _safe(f"{artist} - {name}") # artist_song(默认)
|
||
|
||
|
||
# 高频轮询/流式端点:不写逐请求日志(3s 排队轮询、1.5s 进度轮询、15s 心跳、
|
||
# Range 分段流、错误上报都会在忙碌站点把 5MB×3 的轮转日志几小时刷一圈,排障信息被冲掉)。
|
||
# 用户主动动作(任务列表/取消)保留日志。
|
||
_QUIET_LOG_PATHS = {'/api/queue', '/api/batch_progress', '/api/task/heartbeat',
|
||
'/audio_stream', '/api/fe_error'}
|