2025-08-25 01:23:41 +08:00
|
|
|
|
"""网易云音乐二维码登录模块
|
|
|
|
|
|
|
|
|
|
|
|
提供网易云音乐二维码登录功能,包括:
|
|
|
|
|
|
- 二维码生成和显示
|
|
|
|
|
|
- 登录状态检查
|
2026-06-21 04:03:28 +08:00
|
|
|
|
- Cookie 获取和保存(直接读写 cookie.txt)
|
2025-08-25 01:23:41 +08:00
|
|
|
|
- 用户友好的交互界面
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-06-21 04:03:28 +08:00
|
|
|
|
import os
|
2025-08-25 01:23:41 +08:00
|
|
|
|
import sys
|
|
|
|
|
|
import time
|
2026-06-21 04:03:28 +08:00
|
|
|
|
import shutil
|
2025-08-25 01:23:41 +08:00
|
|
|
|
import logging
|
2026-06-21 04:03:28 +08:00
|
|
|
|
import datetime
|
2025-08-25 01:23:41 +08:00
|
|
|
|
from pathlib import Path
|
2026-06-21 04:03:28 +08:00
|
|
|
|
from typing import Optional, Tuple
|
2025-08-25 01:23:41 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
from music_api import QRLoginManager, APIException, load_cookies
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except ImportError as e:
|
|
|
|
|
|
print(f"导入模块失败: {e}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print("请确保 music_api.py 文件存在且可用")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-21 04:03:28 +08:00
|
|
|
|
# 判断 cookie 是否"有效":包含任一关键字段即可
|
|
|
|
|
|
IMPORTANT_COOKIE_KEYS = ("MUSIC_U", "__csrf", "NMTID")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
class QRLoginClient:
|
|
|
|
|
|
"""二维码登录客户端"""
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
def __init__(self, cookie_file: str = "cookie.txt"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Args:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
cookie_file: Cookie 保存文件路径
|
2025-08-25 01:23:41 +08:00
|
|
|
|
"""
|
2026-06-21 04:03:28 +08:00
|
|
|
|
self.cookie_file = Path(cookie_file)
|
2025-08-25 01:23:41 +08:00
|
|
|
|
self.qr_manager = QRLoginManager()
|
|
|
|
|
|
self.logger = logging.getLogger(__name__)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
if not self.logger.handlers:
|
|
|
|
|
|
handler = logging.StreamHandler()
|
|
|
|
|
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
handler.setFormatter(formatter)
|
|
|
|
|
|
self.logger.addHandler(handler)
|
|
|
|
|
|
self.logger.setLevel(logging.INFO)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
# ----------- Cookie 文件直接操作 -----------
|
|
|
|
|
|
|
|
|
|
|
|
def _read_cookie(self) -> str:
|
|
|
|
|
|
if not self.cookie_file.exists():
|
|
|
|
|
|
return ""
|
2025-08-25 01:23:41 +08:00
|
|
|
|
try:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
return self.cookie_file.read_text(encoding="utf-8").strip()
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
|
self.logger.error(f"读取 Cookie 失败: {e}")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
def _write_cookie(self, cookie: str) -> bool:
|
|
|
|
|
|
if not cookie or not cookie.strip():
|
|
|
|
|
|
self.logger.error("Cookie 内容不能为空")
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.cookie_file.write_text(cookie.strip() + "\n", encoding="utf-8")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
|
self.logger.error(f"写入 Cookie 失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _backup_cookie(self, suffix: str = "backup") -> Optional[Path]:
|
|
|
|
|
|
if not self.cookie_file.exists():
|
|
|
|
|
|
return None
|
|
|
|
|
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
backup_path = self.cookie_file.with_name(
|
|
|
|
|
|
f"{self.cookie_file.stem}.{suffix}.{ts}{self.cookie_file.suffix}"
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
shutil.copy2(self.cookie_file, backup_path)
|
|
|
|
|
|
return backup_path
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
|
self.logger.warning(f"备份 Cookie 失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _clear_cookie(self) -> bool:
|
|
|
|
|
|
if not self.cookie_file.exists():
|
|
|
|
|
|
return True
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.cookie_file.unlink()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
|
self.logger.error(f"清除 Cookie 失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _is_cookie_valid(self) -> bool:
|
|
|
|
|
|
cookies = load_cookies(str(self.cookie_file))
|
|
|
|
|
|
return any(k in cookies for k in IMPORTANT_COOKIE_KEYS)
|
|
|
|
|
|
|
|
|
|
|
|
def _get_cookie_info(self) -> dict:
|
|
|
|
|
|
cookies = load_cookies(str(self.cookie_file))
|
|
|
|
|
|
present = [k for k in IMPORTANT_COOKIE_KEYS if k in cookies]
|
|
|
|
|
|
info = {
|
|
|
|
|
|
"file_path": str(self.cookie_file.absolute()),
|
|
|
|
|
|
"file_exists": self.cookie_file.exists(),
|
|
|
|
|
|
"cookie_count": len(cookies),
|
|
|
|
|
|
"is_valid": bool(present),
|
|
|
|
|
|
"important_cookies_present": present,
|
|
|
|
|
|
"missing_important_cookies": [k for k in IMPORTANT_COOKIE_KEYS if k not in cookies],
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.cookie_file.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
mtime = self.cookie_file.stat().st_mtime
|
|
|
|
|
|
info["last_modified"] = datetime.datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
# ----------- 业务方法 -----------
|
|
|
|
|
|
|
|
|
|
|
|
def check_existing_login(self) -> bool:
|
|
|
|
|
|
"""检查是否已有有效登录"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self._is_cookie_valid():
|
|
|
|
|
|
self.logger.info("检测到有效的登录 Cookie")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
return True
|
2026-06-21 04:03:28 +08:00
|
|
|
|
self.logger.info("未检测到有效的登录 Cookie")
|
|
|
|
|
|
return False
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"检查登录状态失败: {e}")
|
|
|
|
|
|
return False
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
def interactive_login(self) -> Tuple[bool, Optional[str]]:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
"""交互式二维码登录"""
|
2025-08-25 01:23:41 +08:00
|
|
|
|
try:
|
|
|
|
|
|
print("\n=== 网易云音乐二维码登录 ===")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
if self.check_existing_login():
|
|
|
|
|
|
choice = input("检测到已有有效登录,是否重新登录?(y/N): ").strip().lower()
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if choice not in ('y', 'yes', '是'):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("使用现有登录状态")
|
|
|
|
|
|
return True, None
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("\n开始二维码登录流程...")
|
|
|
|
|
|
print("正在生成二维码...")
|
|
|
|
|
|
qr_result = self.qr_manager.create_qr_login()
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
if not qr_result.get('success'):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
error_msg = f"生成二维码失败: {qr_result.get('message', '未知错误')}"
|
|
|
|
|
|
self.logger.error(error_msg)
|
|
|
|
|
|
return False, error_msg
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
qr_key = qr_result['qr_key']
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print("\n二维码已生成!")
|
|
|
|
|
|
print("请使用网易云音乐手机 APP 扫描二维码进行登录")
|
|
|
|
|
|
print("二维码有效期: 3 分钟")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("\n等待扫码中...")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
max_attempts = 60 # 最多 5 分钟
|
2025-08-25 01:23:41 +08:00
|
|
|
|
attempt = 0
|
|
|
|
|
|
while attempt < max_attempts:
|
|
|
|
|
|
try:
|
|
|
|
|
|
status_result = self.qr_manager.check_qr_login(qr_key)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
if status_result.get('success'):
|
|
|
|
|
|
status = status_result.get('status')
|
|
|
|
|
|
if status == 'success':
|
2025-08-25 01:23:41 +08:00
|
|
|
|
cookie = status_result.get('cookie', '')
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if not cookie:
|
|
|
|
|
|
return False, "登录成功但未获取到 Cookie"
|
|
|
|
|
|
if self.save_cookie(cookie):
|
|
|
|
|
|
print("\n✅ 登录成功!Cookie 已保存")
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
return False, "登录成功但 Cookie 保存失败"
|
|
|
|
|
|
|
|
|
|
|
|
if status == 'waiting' and attempt % 10 == 0:
|
|
|
|
|
|
print(f"等待扫码中... ({attempt + 1}/{max_attempts})")
|
|
|
|
|
|
elif status == 'scanned':
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("二维码已扫描,请在手机上确认登录")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
elif status == 'expired':
|
|
|
|
|
|
print("\n❌ 二维码已过期,请重新尝试")
|
|
|
|
|
|
return False, "二维码已过期"
|
|
|
|
|
|
elif status == 'error':
|
|
|
|
|
|
msg = status_result.get('message', '未知错误')
|
|
|
|
|
|
print(f"\n❌ 登录失败: {msg}")
|
|
|
|
|
|
return False, f"登录失败: {msg}"
|
2025-08-25 01:23:41 +08:00
|
|
|
|
else:
|
|
|
|
|
|
self.logger.warning(f"检查登录状态失败: {status_result.get('message', '未知错误')}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
attempt += 1
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n用户取消登录")
|
|
|
|
|
|
return False, "用户取消登录"
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"检查登录状态时发生错误: {e}")
|
|
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
attempt += 1
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
print("\n❌ 登录超时,请重新尝试")
|
|
|
|
|
|
return False, "登录超时"
|
|
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except APIException as e:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
self.logger.error(f"API 调用失败: {e}")
|
|
|
|
|
|
return False, f"API 调用失败: {e}"
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except Exception as e:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
self.logger.error(f"登录过程中发生未知错误: {e}")
|
|
|
|
|
|
return False, f"登录过程中发生未知错误: {e}"
|
|
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
def save_cookie(self, cookie: str) -> bool:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
"""保存 Cookie 到文件"""
|
2025-08-25 01:23:41 +08:00
|
|
|
|
try:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if self.cookie_file.exists():
|
|
|
|
|
|
backup = self._backup_cookie()
|
|
|
|
|
|
if backup:
|
|
|
|
|
|
self.logger.info(f"已备份现有 Cookie 到: {backup}")
|
|
|
|
|
|
|
|
|
|
|
|
if not self._write_cookie(cookie):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
return False
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
if self._is_cookie_valid():
|
|
|
|
|
|
self.logger.info("Cookie 保存并验证成功")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
self.logger.warning("Cookie 保存成功但验证失败(缺少关键字段)")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
self.logger.error(f"保存 Cookie 时发生错误: {e}")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
return False
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
def show_login_info(self) -> None:
|
|
|
|
|
|
"""显示登录信息"""
|
|
|
|
|
|
try:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
info = self._get_cookie_info()
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("\n=== 登录状态信息 ===")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print(f"Cookie 文件: {info['file_path']}")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print(f"文件存在: {'是' if info['file_exists'] else '否'}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print(f"Cookie 数量: {info['cookie_count']}")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print(f"登录状态: {'有效' if info['is_valid'] else '无效'}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if info.get("last_modified"):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print(f"最后更新: {info['last_modified']}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if info["is_valid"]:
|
|
|
|
|
|
present = info.get("important_cookies_present", [])
|
|
|
|
|
|
print(f"重要 Cookie: {', '.join(present)}")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
else:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
missing = info.get("missing_important_cookies", [])
|
|
|
|
|
|
if missing:
|
|
|
|
|
|
print(f"缺少 Cookie: {', '.join(missing)}")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取登录信息失败: {e}")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
def logout(self) -> bool:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
"""登出(清除 Cookie)"""
|
2025-08-25 01:23:41 +08:00
|
|
|
|
try:
|
2026-06-21 04:03:28 +08:00
|
|
|
|
if self.cookie_file.exists():
|
|
|
|
|
|
backup = self._backup_cookie("logout")
|
|
|
|
|
|
if backup:
|
|
|
|
|
|
print(f"Cookie 已备份到: {backup}")
|
|
|
|
|
|
if self._clear_cookie():
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("已成功登出")
|
|
|
|
|
|
return True
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print("登出失败")
|
|
|
|
|
|
return False
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"登出时发生错误: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
|
|
|
|
)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
client = QRLoginClient()
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
|
|
command = sys.argv[1].lower()
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
if command == 'login':
|
|
|
|
|
|
success, error = client.interactive_login()
|
|
|
|
|
|
if success:
|
|
|
|
|
|
print("\n登录完成!")
|
|
|
|
|
|
client.show_login_info()
|
|
|
|
|
|
sys.exit(0)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print(f"\n登录失败: {error}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
elif command in ('status', 'info'):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
client.show_login_info()
|
|
|
|
|
|
sys.exit(0)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
elif command == 'logout':
|
2026-06-21 04:03:28 +08:00
|
|
|
|
sys.exit(0 if client.logout() else 1)
|
|
|
|
|
|
|
|
|
|
|
|
elif command in ('help', '-h', '--help'):
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("网易云音乐二维码登录工具")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print("\n用法: python qr_login.py [命令]")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print("\n命令:")
|
|
|
|
|
|
print(" login - 执行二维码登录")
|
|
|
|
|
|
print(" status - 显示登录状态")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
print(" logout - 登出(清除 Cookie)")
|
2025-08-25 01:23:41 +08:00
|
|
|
|
print(" help - 显示此帮助信息")
|
|
|
|
|
|
print("\n如果不提供命令,将进入交互模式")
|
|
|
|
|
|
sys.exit(0)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
else:
|
|
|
|
|
|
print(f"未知命令: {command}")
|
|
|
|
|
|
print("使用 'python qr_login.py help' 查看帮助")
|
|
|
|
|
|
sys.exit(1)
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
# 交互模式
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
print("\n=== 网易云音乐登录工具 ===")
|
|
|
|
|
|
print("1. 二维码登录")
|
|
|
|
|
|
print("2. 查看登录状态")
|
|
|
|
|
|
print("3. 登出")
|
|
|
|
|
|
print("4. 退出")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
choice = input("\n请选择操作 (1-4): ").strip()
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
if choice == '1':
|
|
|
|
|
|
success, error = client.interactive_login()
|
|
|
|
|
|
if success:
|
|
|
|
|
|
print("\n登录成功!")
|
|
|
|
|
|
client.show_login_info()
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"\n登录失败: {error}")
|
|
|
|
|
|
elif choice == '2':
|
|
|
|
|
|
client.show_login_info()
|
|
|
|
|
|
elif choice == '3':
|
|
|
|
|
|
client.logout()
|
|
|
|
|
|
elif choice == '4':
|
|
|
|
|
|
print("再见!")
|
|
|
|
|
|
break
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("无效选择,请重试")
|
2026-06-21 04:03:28 +08:00
|
|
|
|
|
2025-08-25 01:23:41 +08:00
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n\n程序已退出")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"\n程序运行出错: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|