diff --git a/core/db.py b/core/db.py index 35dd59a..913e453 100644 --- a/core/db.py +++ b/core/db.py @@ -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]: """写事务上下文:多条写语句原子提交(串行锁内)。 diff --git a/core/repo.py b/core/repo.py index ddbb53c..589c0f5 100644 --- a/core/repo.py +++ b/core/repo.py @@ -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 diff --git a/core/utils.py b/core/utils.py index 58222f9..816ad80 100644 --- a/core/utils.py +++ b/core/utils.py @@ -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) diff --git a/services/_shared.py b/services/_shared.py index f3120a7..396ece3 100644 --- a/services/_shared.py +++ b/services/_shared.py @@ -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 的副本均已删除——三份实现零调用) # ============ 接口请求统计 ============ diff --git a/services/api_service.py b/services/api_service.py index d961cd3..5c50181 100644 --- a/services/api_service.py +++ b/services/api_service.py @@ -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服务类""" diff --git a/services/downloads.py b/services/downloads.py index 6580670..c8e2384 100644 --- a/services/downloads.py +++ b/services/downloads.py @@ -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: """把共享缓存文件复制/硬链接为任务专属交付文件(任务成果与共享缓存分离, 任何任务的清理都不会再删掉别的任务的成果)""" diff --git a/web/auth_state.py b/web/auth_state.py index 1c071ef..fb89a35 100644 --- a/web/auth_state.py +++ b/web/auth_state.py @@ -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 diff --git a/web/request_utils.py b/web/request_utils.py index 519584f..ec23fbc 100644 --- a/web/request_utils.py +++ b/web/request_utils.py @@ -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