Files
Netease_url/qr_login.py
T
Suxiaoqinx 3b3383cde5 feat: 新增杜比全景声支持,重构Cookie管理与项目配置
- 添加前端页面的杜比全景声音质选择选项,更新后端音质参数验证列表
- 适配杜比全景声的编码格式,将dolby音质的encodeType设为mp4
- 重构Cookie管理模块:移除独立的cookie_manager.py,将Cookie读写、解析逻辑迁移至music_api.py,简化qr_login工具实现
- 更新Dockerfile,添加ffmpeg依赖以支持音频转码处理
- 更新requirements.txt依赖配置
- 更新使用文档,调整项目文件结构说明
- 优化项目依赖,移除冗余的cookie_manager模块依赖
2026-06-21 04:03:28 +08:00

351 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""网易云音乐二维码登录模块
提供网易云音乐二维码登录功能,包括:
- 二维码生成和显示
- 登录状态检查
- Cookie 获取和保存(直接读写 cookie.txt)
- 用户友好的交互界面
"""
import os
import sys
import time
import shutil
import logging
import datetime
from pathlib import Path
from typing import Optional, Tuple
try:
from music_api import QRLoginManager, APIException, load_cookies
except ImportError as e:
print(f"导入模块失败: {e}")
print("请确保 music_api.py 文件存在且可用")
sys.exit(1)
# 判断 cookie 是否"有效":包含任一关键字段即可
IMPORTANT_COOKIE_KEYS = ("MUSIC_U", "__csrf", "NMTID")
class QRLoginClient:
"""二维码登录客户端"""
def __init__(self, cookie_file: str = "cookie.txt"):
"""
Args:
cookie_file: Cookie 保存文件路径
"""
self.cookie_file = Path(cookie_file)
self.qr_manager = QRLoginManager()
self.logger = logging.getLogger(__name__)
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)
# ----------- Cookie 文件直接操作 -----------
def _read_cookie(self) -> str:
if not self.cookie_file.exists():
return ""
try:
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")
return True
self.logger.info("未检测到有效的登录 Cookie")
return False
except Exception as e:
self.logger.error(f"检查登录状态失败: {e}")
return False
def interactive_login(self) -> Tuple[bool, Optional[str]]:
"""交互式二维码登录"""
try:
print("\n=== 网易云音乐二维码登录 ===")
if self.check_existing_login():
choice = input("检测到已有有效登录,是否重新登录?(y/N): ").strip().lower()
if choice not in ('y', 'yes', '是'):
print("使用现有登录状态")
return True, None
print("\n开始二维码登录流程...")
print("正在生成二维码...")
qr_result = self.qr_manager.create_qr_login()
if not qr_result.get('success'):
error_msg = f"生成二维码失败: {qr_result.get('message', '未知错误')}"
self.logger.error(error_msg)
return False, error_msg
qr_key = qr_result['qr_key']
print("\n二维码已生成!")
print("请使用网易云音乐手机 APP 扫描二维码进行登录")
print("二维码有效期: 3 分钟")
print("\n等待扫码中...")
max_attempts = 60 # 最多 5 分钟
attempt = 0
while attempt < max_attempts:
try:
status_result = self.qr_manager.check_qr_login(qr_key)
if status_result.get('success'):
status = status_result.get('status')
if status == 'success':
cookie = status_result.get('cookie', '')
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':
print("二维码已扫描,请在手机上确认登录")
elif status == 'expired':
print("\n❌ 二维码已过期,请重新尝试")
return False, "二维码已过期"
elif status == 'error':
msg = status_result.get('message', '未知错误')
print(f"\n❌ 登录失败: {msg}")
return False, f"登录失败: {msg}"
else:
self.logger.warning(f"检查登录状态失败: {status_result.get('message', '未知错误')}")
time.sleep(5)
attempt += 1
except KeyboardInterrupt:
print("\n用户取消登录")
return False, "用户取消登录"
except Exception as e:
self.logger.error(f"检查登录状态时发生错误: {e}")
time.sleep(5)
attempt += 1
print("\n❌ 登录超时,请重新尝试")
return False, "登录超时"
except APIException as e:
self.logger.error(f"API 调用失败: {e}")
return False, f"API 调用失败: {e}"
except Exception as e:
self.logger.error(f"登录过程中发生未知错误: {e}")
return False, f"登录过程中发生未知错误: {e}"
def save_cookie(self, cookie: str) -> bool:
"""保存 Cookie 到文件"""
try:
if self.cookie_file.exists():
backup = self._backup_cookie()
if backup:
self.logger.info(f"已备份现有 Cookie 到: {backup}")
if not self._write_cookie(cookie):
return False
if self._is_cookie_valid():
self.logger.info("Cookie 保存并验证成功")
return True
self.logger.warning("Cookie 保存成功但验证失败(缺少关键字段)")
return False
except Exception as e:
self.logger.error(f"保存 Cookie 时发生错误: {e}")
return False
def show_login_info(self) -> None:
"""显示登录信息"""
try:
info = self._get_cookie_info()
print("\n=== 登录状态信息 ===")
print(f"Cookie 文件: {info['file_path']}")
print(f"文件存在: {'是' if info['file_exists'] else '否'}")
print(f"Cookie 数量: {info['cookie_count']}")
print(f"登录状态: {'有效' if info['is_valid'] else '无效'}")
if info.get("last_modified"):
print(f"最后更新: {info['last_modified']}")
if info["is_valid"]:
present = info.get("important_cookies_present", [])
print(f"重要 Cookie: {', '.join(present)}")
else:
missing = info.get("missing_important_cookies", [])
if missing:
print(f"缺少 Cookie: {', '.join(missing)}")
except Exception as e:
print(f"获取登录信息失败: {e}")
def logout(self) -> bool:
"""登出(清除 Cookie)"""
try:
if self.cookie_file.exists():
backup = self._backup_cookie("logout")
if backup:
print(f"Cookie 已备份到: {backup}")
if self._clear_cookie():
print("已成功登出")
return True
print("登出失败")
return False
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'
)
client = QRLoginClient()
if len(sys.argv) > 1:
command = sys.argv[1].lower()
if command == 'login':
success, error = client.interactive_login()
if success:
print("\n登录完成!")
client.show_login_info()
sys.exit(0)
print(f"\n登录失败: {error}")
sys.exit(1)
elif command in ('status', 'info'):
client.show_login_info()
sys.exit(0)
elif command == 'logout':
sys.exit(0 if client.logout() else 1)
elif command in ('help', '-h', '--help'):
print("网易云音乐二维码登录工具")
print("\n用法: python qr_login.py [命令]")
print("\n命令:")
print(" login - 执行二维码登录")
print(" status - 显示登录状态")
print(" logout - 登出(清除 Cookie)")
print(" help - 显示此帮助信息")
print("\n如果不提供命令,将进入交互模式")
sys.exit(0)
else:
print(f"未知命令: {command}")
print("使用 'python qr_login.py help' 查看帮助")
sys.exit(1)
# 交互模式
try:
while True:
print("\n=== 网易云音乐登录工具 ===")
print("1. 二维码登录")
print("2. 查看登录状态")
print("3. 登出")
print("4. 退出")
choice = input("\n请选择操作 (1-4): ").strip()
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("无效选择,请重试")
except KeyboardInterrupt:
print("\n\n程序已退出")
sys.exit(0)
except Exception as e:
print(f"\n程序运行出错: {e}")
sys.exit(1)
if __name__ == "__main__":
main()