refactor(async): music_api 异步化 + 全项目 lint 收敛

music_api.py:
- 22 个方法改 async(HTTPClient.post_request*/NeteaseAPI.*/QRLoginManager.*),
  33 处 requests.* → await core.aiohttp_compat.*
- 16 个模块级同步包装改走 run_sync 桥(main.py 等 99 个同步调用点零改动)
- async 内 time.sleep → asyncio.sleep(修 7 处阻塞事件循环)

musica_downloader/qr_login:
- 单曲三请求 ThreadPoolExecutor → asyncio.gather(语义等价,失败处理保留)
- interactive_login 改 async,CLI 入口用 asyncio.run

真实缺陷修复(lint 发现):
- podcast_api._to_int 未定义(2 处调用会 NameError)→ 补安全转换 helper
- main.py VALID_QUALITY_LEVELS 重复定义(覆盖 core.config 导入值)→ 移除
- 移除死代码变量(salt×2/device_id×3/file_path/zipped_files)

lint 收敛:524→0(ruff All checks passed);pyright 0 errors。
pyproject 记录「有意保留」项(B904/RUF012/S305 上游协议等)及理由。

验证:冒烟 10/10;pytest 16/16;真实上游 get_toplist 异步直调与桥接均得 63 条。
This commit is contained in:
snowy-yang committed 2026-09-26 00:32:45 +08:00
1 parent 4962542e78
commit ce7ea66d6a
8 files changed
+520 -490

No files matched your search

+39 -32
View File
@@ -19,8 +19,7 @@
import json
import re
import time
from random import randrange
from typing import Dict, List, Optional, Any
from typing import Any
import requests
@@ -54,7 +53,7 @@ class PodcastConstants:
_MAX_UPSTREAM_JSON_BYTES = 8 * 1024 * 1024
def _post_json(url: str, data: Dict[str, Any], cookies: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
def _post_json(url: str, data: dict[str, Any], cookies: dict[str, str] | None = None) -> dict[str, Any]:
"""发送表单 POST 请求并返回 JSON"""
headers = {
'User-Agent': APIConstants.USER_AGENT,
@@ -78,7 +77,15 @@ def _post_json(url: str, data: Dict[str, Any], cookies: Optional[Dict[str, str]]
return result
def _ts_to_date(ts: Optional[int]) -> str:
def _to_int(v: Any, default: int | None = None) -> int | None:
"""安全转 int:垃圾值/None 回退默认值(上游字段常为 null/字符串)。"""
try:
return int(v)
except (TypeError, ValueError):
return default
def _ts_to_date(ts: int | None) -> str:
"""毫秒时间戳 → 日期字符串"""
try:
if not ts:
@@ -88,7 +95,7 @@ def _ts_to_date(ts: Optional[int]) -> str:
return ''
def _fmt_duration(ms: Optional[int]) -> str:
def _fmt_duration(ms: int | None) -> str:
"""毫秒 → mm:ss / hh:mm:ss"""
try:
sec = int((ms or 0) / 1000)
@@ -101,7 +108,7 @@ def _fmt_duration(ms: Optional[int]) -> str:
return f"{h}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"
def _fmt_count(n: Optional[int]) -> str:
def _fmt_count(n: int | None) -> str:
"""数量 → 万单位简写"""
try:
n = int(n or 0)
@@ -118,7 +125,7 @@ def _fmt_count(n: Optional[int]) -> str:
# ============ 数据规范化 ============
def _norm_radio(raw: Dict[str, Any]) -> Dict[str, Any]:
def _norm_radio(raw: dict[str, Any]) -> dict[str, Any]:
"""规范化电台(播客)信息"""
dj = raw.get('dj') or {}
return {
@@ -139,7 +146,7 @@ def _norm_radio(raw: Dict[str, Any]) -> Dict[str, Any]:
}
def _norm_program(raw: Dict[str, Any]) -> Dict[str, Any]:
def _norm_program(raw: dict[str, Any]) -> dict[str, Any]:
"""规范化单集(program)信息"""
radio = raw.get('radio') or {}
dj = raw.get('dj') or radio.get('dj') or {}
@@ -168,7 +175,7 @@ def _norm_program(raw: Dict[str, Any]) -> Dict[str, Any]:
# ============ 链接 / ID 解析 ============
_SHORTLINK_CACHE: Dict[str, str] = {}
_SHORTLINK_CACHE: dict[str, str] = {}
def _resolve_short_link(url: str) -> str:
@@ -201,7 +208,7 @@ def _resolve_short_link(url: str) -> str:
return real
def extract_podcast_ref(raw: str) -> Dict[str, str]:
def extract_podcast_ref(raw: str) -> dict[str, str]:
"""从用户输入中提取播客引用(T4:正规 URL 解析,替代子串匹配与非数字剔除)
Returns:
@@ -210,7 +217,7 @@ def extract_podcast_ref(raw: str) -> Dict[str, str]:
- program: program?id= / sound?id= 链接
- unknown: 纯数字 ID(需依次尝试单集/电台)
"""
from urllib.parse import urlsplit, parse_qs
from urllib.parse import parse_qs, urlsplit
text = str(raw or '').strip()
# 短链先还原(仅当整串就是一个短链 URL)
if '163cn.tv' in text.lower():
@@ -247,9 +254,9 @@ def extract_podcast_ref(raw: str) -> Dict[str, str]:
# ============ 核心 API ============
def search_podcast(keywords: str, cookies: Optional[Dict[str, str]] = None,
def search_podcast(keywords: str, cookies: dict[str, str] | None = None,
limit: int = 20, offset: int = 0,
search_type: str = 'all') -> Dict[str, Any]:
search_type: str = 'all') -> dict[str, Any]:
"""搜索播客
Args:
@@ -267,9 +274,9 @@ def search_podcast(keywords: str, cookies: Optional[Dict[str, str]] = None,
raise APIException('搜索关键词不能为空')
limit = max(1, min(int(limit or 20), 50))
result: Dict[str, Any] = {'radios': [], 'voices': []}
result: dict[str, Any] = {'radios': [], 'voices': []}
def _do_search(stype: int) -> Dict[str, Any]:
def _do_search(stype: int) -> dict[str, Any]:
return _post_json(PodcastConstants.SEARCH_API,
{'s': keywords, 'type': stype, 'limit': limit, 'offset': offset},
cookies)
@@ -288,7 +295,7 @@ def search_podcast(keywords: str, cookies: Optional[Dict[str, str]] = None,
return result
def get_radio_detail(radio_id: Any, cookies: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
def get_radio_detail(radio_id: Any, cookies: dict[str, str] | None = None) -> dict[str, Any]:
"""获取播客电台(djradio)详情"""
try:
rid = int(str(radio_id).strip())
@@ -302,8 +309,8 @@ def get_radio_detail(radio_id: Any, cookies: Optional[Dict[str, str]] = None) ->
return _norm_radio(data)
def get_radio_programs(radio_id: Any, cookies: Optional[Dict[str, str]] = None,
limit: int = 30, offset: int = 0, asc: bool = True) -> Dict[str, Any]:
def get_radio_programs(radio_id: Any, cookies: dict[str, str] | None = None,
limit: int = 30, offset: int = 0, asc: bool = True) -> dict[str, Any]:
"""获取电台下的单集列表(分页)
Args:
@@ -334,7 +341,7 @@ def get_radio_programs(radio_id: Any, cookies: Optional[Dict[str, str]] = None,
}
def get_program_detail(program_id: Any, cookies: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
def get_program_detail(program_id: Any, cookies: dict[str, str] | None = None) -> dict[str, Any]:
"""获取单集(program)详情"""
try:
pid = int(str(program_id).strip())
@@ -347,7 +354,7 @@ def get_program_detail(program_id: Any, cookies: Optional[Dict[str, str]] = None
return _norm_program(program)
def get_categories() -> List[Dict[str, Any]]:
def get_categories() -> list[dict[str, Any]]:
"""获取播客分类列表(GET 接口)
Returns:
@@ -387,9 +394,9 @@ def get_categories() -> List[Dict[str, Any]]:
return cats
def get_hot_radios(cookies: Optional[Dict[str, str]] = None,
def get_hot_radios(cookies: dict[str, str] | None = None,
limit: int = 30, offset: int = 0,
cate_id: Optional[int] = None) -> List[Dict[str, Any]]:
cate_id: int | None = None) -> list[dict[str, Any]]:
"""获取分类下热门播客
Args:
@@ -400,7 +407,7 @@ def get_hot_radios(cookies: Optional[Dict[str, str]] = None,
电台列表(已规范化)
"""
limit = max(1, min(int(limit or 30), 100))
cate_ids: List[int] = []
cate_ids: list[int] = []
user_cate = False
if cate_id:
try:
@@ -425,8 +432,8 @@ def get_hot_radios(cookies: Optional[Dict[str, str]] = None,
return []
def resolve_episode_url(song_id: Any, cookies: Optional[Dict[str, str]] = None,
level: str = 'auto') -> Optional[Dict[str, Any]]:
def resolve_episode_url(song_id: Any, cookies: dict[str, str] | None = None,
level: str = 'auto') -> dict[str, Any] | None:
"""解析单集音频直链(带音质降级)
Args:
@@ -439,7 +446,7 @@ def resolve_episode_url(song_id: Any, cookies: Optional[Dict[str, str]] = None,
"""
# F83:降级阶梯只向下不向上——原实现指定 standard 会回退到更高的 exhigh
_FALLDOWN_LADDER = ['hires', 'lossless', 'exhigh', 'standard']
order: List[str] = []
order: list[str] = []
level = str(level or 'auto').strip()
if level and level != 'auto':
order.append(level)
@@ -454,7 +461,7 @@ def resolve_episode_url(song_id: Any, cookies: Optional[Dict[str, str]] = None,
else:
order.extend(PodcastConstants.QUALITY_LADDER)
# 去重保序
dedup: List[str] = []
dedup: list[str] = []
for lv in order:
if lv not in dedup:
dedup.append(lv)
@@ -487,8 +494,8 @@ def resolve_episode_url(song_id: Any, cookies: Optional[Dict[str, str]] = None,
# ============ 组合能力 ============
def parse_podcast_ref(raw: str, cookies: Optional[Dict[str, str]] = None,
page_size: int = 30) -> Dict[str, Any]:
def parse_podcast_ref(raw: str, cookies: dict[str, str] | None = None,
page_size: int = 30) -> dict[str, Any]:
"""智能解析用户输入:电台链接 → 电台详情+单集列表;单集链接 → 单集详情
Returns:
@@ -515,15 +522,15 @@ def parse_podcast_ref(raw: str, cookies: Optional[Dict[str, str]] = None,
return {'kind': 'radio', 'radio': radio, **page}
def list_radio_all_program_ids(radio_id: Any, cookies: Optional[Dict[str, str]] = None,
max_count: int = 200) -> List[Dict[str, Any]]:
def list_radio_all_program_ids(radio_id: Any, cookies: dict[str, str] | None = None,
max_count: int = 200) -> list[dict[str, Any]]:
"""拉取电台全部单集(用于整台打包,正序),返回 [{'id','name','main_song_id','serial_num','cover_url'}]
最多拉取 max_count 集,防止超大电台拖垮磁盘/任务。
F98:按节目 ID 去重;一页没有任何新增(短页/重复页/列表变动)即有界停止,
不再固定 offset+=100 盲目翻页。
"""
out: List[Dict[str, Any]] = []
out: list[dict[str, Any]] = []
seen_ids = set()
offset = 0
page_size = 100