feat: 新增杜比全景声支持,重构Cookie管理与项目配置

- 添加前端页面的杜比全景声音质选择选项,更新后端音质参数验证列表
- 适配杜比全景声的编码格式,将dolby音质的encodeType设为mp4
- 重构Cookie管理模块:移除独立的cookie_manager.py,将Cookie读写、解析逻辑迁移至music_api.py,简化qr_login工具实现
- 更新Dockerfile,添加ffmpeg依赖以支持音频转码处理
- 更新requirements.txt依赖配置
- 更新使用文档,调整项目文件结构说明
- 优化项目依赖,移除冗余的cookie_manager模块依赖
This commit is contained in:
Suxiaoqinx
2026-06-21 04:03:28 +08:00
parent 682200c705
commit 3b3383cde5
10 changed files with 823 additions and 1128 deletions
+1
View File
@@ -1,5 +1,6 @@
FROM python:3.9.22-alpine3.21 FROM python:3.9.22-alpine3.21
WORKDIR /app WORKDIR /app
RUN apk add --no-cache ffmpeg
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \ RUN pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip3 install --no-cache-dir -r requirements.txt pip3 install --no-cache-dir -r requirements.txt
+1 -1
View File
@@ -1 +1 @@
MUSIC_U=1eb9ce22024bb666e99b6743b2222f29ef64a9e88fda0fd5754714b900a5d70d993166e004087dd3b95085f6a85b059f5e9aba41e3f2646e3cebdbec0317df58c119e5;os=pc;appver=8.9.75; MUSIC_U=00C09F150B1C965F6D668A94D0FBC54690D76D5A2043282656875EAE3F0B99B667F736421093E1B4AF4E3DF93C69F7D40C582783F8A1CCA17D1B701944878BBC29AAE08CDF8017734DF9D12275011772CDA37A08D560D2910FD3367F28E5FE7F69BBD339DDF371212A28E7FDCBD48758B2F13D9D4A96BFED34C4458755B59342895B8C92E0A28984445BC2A64F35B6EA550B280CD20F87BC10636369C0F3CB71BC68B3A55542334B314BF90FA460B4032B2F298CAA4522E445D665B8A498FD4A0CC9FCD3E8AD0A775609B56C7772E957000419B67B74480CFCCE61E78AAEB8E851E57B8F878C4E76095F6ACBB3247788DBF594265E30E2E668102EB9160299921775D563E2EC7A40D29E58482B6AC2A728980F76632571D5AABCA86169E79EA9BBCC054E5A2CE26FBC3723086EC5D3F9BC081BBB35A17B796283E8712AA64F3B2CA952F76A0C1CA2CD8F6A524B05142175CF2542D2214341A0B328566888F79130A4074D82D2E60C551A3582275ABBDEE9059C5240E5CD2C1703ACABA4D15DAF466D0E8A8604CA4A11A4496D7630478FB0E662E835A6D2A6B3EB2596191E3E5CCF;appver=8.9.75;
-469
View File
@@ -1,469 +0,0 @@
"""Cookie管理器模块
提供网易云音乐Cookie管理功能,包括:
- Cookie文件读取和写入
- Cookie格式验证和解析
- Cookie有效性检查
- 自动过期处理
"""
import os
import json
import time
from typing import Dict, Optional, List, Tuple, Any
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
@dataclass
class CookieInfo:
"""Cookie信息数据类"""
name: str
value: str
domain: str = ""
path: str = "/"
expires: Optional[int] = None
secure: bool = False
http_only: bool = False
class CookieException(Exception):
"""Cookie相关异常类"""
pass
class CookieManager:
"""Cookie管理器主类"""
def __init__(self, cookie_file: str = "cookie.txt"):
"""
初始化Cookie管理器
Args:
cookie_file: Cookie文件路径
"""
self.cookie_file = Path(cookie_file)
self.logger = logging.getLogger(__name__)
# 网易云音乐相关的重要Cookie字段
self.important_cookies = {
'MUSIC_U', # 用户标识
'MUSIC_A', # 用户认证
'__csrf', # CSRF令牌
'NMTID', # 设备标识
'WEVNSM', # 会话管理
'WNMCID', # 客户端标识
}
# 确保cookie文件存在
self._ensure_cookie_file_exists()
def _ensure_cookie_file_exists(self) -> None:
"""确保Cookie文件存在"""
if not self.cookie_file.exists():
self.cookie_file.touch()
self.logger.info(f"创建Cookie文件: {self.cookie_file}")
def read_cookie(self) -> str:
"""读取Cookie文件内容
Returns:
Cookie字符串内容
Raises:
CookieException: 读取失败时抛出
"""
try:
if not self.cookie_file.exists():
self.logger.warning(f"Cookie文件不存在: {self.cookie_file}")
return ""
content = self.cookie_file.read_text(encoding='utf-8').strip()
if not content:
self.logger.warning("Cookie文件为空")
return ""
self.logger.debug(f"成功读取Cookie文件,长度: {len(content)}")
return content
except UnicodeDecodeError as e:
raise CookieException(f"Cookie文件编码错误: {e}")
except PermissionError as e:
raise CookieException(f"没有权限读取Cookie文件: {e}")
except Exception as e:
raise CookieException(f"读取Cookie文件失败: {e}")
def write_cookie(self, cookie_content: str) -> bool:
"""写入Cookie到文件
Args:
cookie_content: Cookie内容字符串
Returns:
是否写入成功
Raises:
CookieException: 写入失败时抛出
"""
try:
if not cookie_content or not cookie_content.strip():
raise CookieException("Cookie内容不能为空")
# 验证Cookie格式
if not self.validate_cookie_format(cookie_content):
raise CookieException("Cookie格式无效")
# 写入文件
self.cookie_file.write_text(cookie_content.strip(), encoding='utf-8')
self.logger.info(f"成功写入Cookie到文件: {self.cookie_file}")
return True
except PermissionError as e:
raise CookieException(f"没有权限写入Cookie文件: {e}")
except Exception as e:
raise CookieException(f"写入Cookie文件失败: {e}")
def parse_cookies(self) -> Dict[str, str]:
"""解析Cookie字符串为字典
Returns:
Cookie字典
Raises:
CookieException: 解析失败时抛出
"""
try:
cookie_content = self.read_cookie()
if not cookie_content:
return {}
return self.parse_cookie_string(cookie_content)
except Exception as e:
raise CookieException(f"解析Cookie失败: {e}")
def parse_cookie_string(self, cookie_string: str) -> Dict[str, str]:
"""解析Cookie字符串
Args:
cookie_string: Cookie字符串
Returns:
Cookie字典
"""
if not cookie_string or not cookie_string.strip():
return {}
cookies = {}
try:
# 处理多种Cookie格式
cookie_string = cookie_string.strip()
# 分割Cookie项
cookie_pairs = []
if ';' in cookie_string:
cookie_pairs = cookie_string.split(';')
elif '\n' in cookie_string:
cookie_pairs = cookie_string.split('\n')
else:
cookie_pairs = [cookie_string]
for pair in cookie_pairs:
pair = pair.strip()
if not pair or '=' not in pair:
continue
# 分割键值对
key, value = pair.split('=', 1)
key = key.strip()
value = value.strip()
if key and value:
cookies[key] = value
self.logger.debug(f"解析得到 {len(cookies)} 个Cookie项")
return cookies
except Exception as e:
self.logger.error(f"解析Cookie字符串失败: {e}")
return {}
def validate_cookie_format(self, cookie_string: str) -> bool:
"""验证Cookie格式是否有效
Args:
cookie_string: Cookie字符串
Returns:
是否格式有效
"""
if not cookie_string or not cookie_string.strip():
return False
try:
# 尝试解析Cookie
cookies = self.parse_cookie_string(cookie_string)
# 检查是否至少包含一个有效的Cookie
if not cookies:
return False
# 检查Cookie名称是否合法
for name, value in cookies.items():
if not name or not isinstance(name, str):
return False
if not isinstance(value, str):
return False
# 检查是否包含非法字符
if any(char in name for char in [' ', '\t', '\n', '\r', ';', ',']):
return False
return True
except Exception:
return False
def is_cookie_valid(self) -> bool:
"""检查Cookie是否有效
Returns:
Cookie是否有效
"""
try:
cookies = self.parse_cookies()
if not cookies:
self.logger.warning("Cookie为空")
return False
# 检查重要Cookie是否存在
missing_cookies = self.important_cookies - set(cookies.keys())
if missing_cookies:
self.logger.warning(f"缺少重要Cookie: {missing_cookies}")
return False
# 检查MUSIC_U是否有效(基本验证)
music_u = cookies.get('MUSIC_U', '')
if not music_u or len(music_u) < 10:
self.logger.warning("MUSIC_U Cookie无效")
return False
self.logger.debug("Cookie验证通过")
return True
except Exception as e:
self.logger.error(f"Cookie验证失败: {e}")
return False
def get_cookie_info(self) -> Dict[str, Any]:
"""获取Cookie详细信息
Returns:
包含Cookie信息的字典
"""
try:
cookies = self.parse_cookies()
info = {
'file_path': str(self.cookie_file),
'file_exists': self.cookie_file.exists(),
'file_size': self.cookie_file.stat().st_size if self.cookie_file.exists() else 0,
'cookie_count': len(cookies),
'is_valid': self.is_cookie_valid(),
'important_cookies_present': list(self.important_cookies & set(cookies.keys())),
'missing_important_cookies': list(self.important_cookies - set(cookies.keys())),
'all_cookie_names': list(cookies.keys())
}
# 添加文件修改时间
if self.cookie_file.exists():
mtime = self.cookie_file.stat().st_mtime
info['last_modified'] = datetime.fromtimestamp(mtime).isoformat()
return info
except Exception as e:
return {
'error': str(e),
'file_path': str(self.cookie_file),
'file_exists': False,
'is_valid': False
}
def backup_cookie(self, backup_suffix: str = None) -> str:
"""备份Cookie文件
Args:
backup_suffix: 备份文件后缀,默认使用时间戳
Returns:
备份文件路径
Raises:
CookieException: 备份失败时抛出
"""
try:
if not self.cookie_file.exists():
raise CookieException("Cookie文件不存在,无法备份")
if backup_suffix is None:
backup_suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = self.cookie_file.with_suffix(f".{backup_suffix}.bak")
# 复制文件内容
content = self.cookie_file.read_text(encoding='utf-8')
backup_path.write_text(content, encoding='utf-8')
self.logger.info(f"Cookie备份成功: {backup_path}")
return str(backup_path)
except Exception as e:
raise CookieException(f"备份Cookie文件失败: {e}")
def restore_cookie(self, backup_path: str) -> bool:
"""从备份恢复Cookie
Args:
backup_path: 备份文件路径
Returns:
是否恢复成功
Raises:
CookieException: 恢复失败时抛出
"""
try:
backup_file = Path(backup_path)
if not backup_file.exists():
raise CookieException(f"备份文件不存在: {backup_path}")
# 读取备份内容
backup_content = backup_file.read_text(encoding='utf-8')
# 验证备份内容
if not self.validate_cookie_format(backup_content):
raise CookieException("备份文件中的Cookie格式无效")
# 写入当前Cookie文件
self.write_cookie(backup_content)
self.logger.info(f"从备份恢复Cookie成功: {backup_path}")
return True
except Exception as e:
raise CookieException(f"恢复Cookie失败: {e}")
def clear_cookie(self) -> bool:
"""清空Cookie文件
Returns:
是否清空成功
"""
try:
if self.cookie_file.exists():
self.cookie_file.write_text("", encoding='utf-8')
self.logger.info("Cookie文件已清空")
return True
except Exception as e:
self.logger.error(f"清空Cookie文件失败: {e}")
return False
def update_cookie(self, new_cookies: Dict[str, str]) -> bool:
"""更新Cookie
Args:
new_cookies: 新的Cookie字典
Returns:
是否更新成功
"""
try:
if not new_cookies:
raise CookieException("新Cookie不能为空")
# 读取现有Cookie
existing_cookies = self.parse_cookies()
# 合并Cookie
existing_cookies.update(new_cookies)
# 转换为Cookie字符串
cookie_string = '; '.join(f"{k}={v}" for k, v in existing_cookies.items())
# 写入文件
return self.write_cookie(cookie_string)
except Exception as e:
self.logger.error(f"更新Cookie失败: {e}")
return False
def get_cookie_for_request(self) -> Dict[str, str]:
"""获取用于HTTP请求的Cookie字典
Returns:
适用于requests库的Cookie字典
"""
try:
cookies = self.parse_cookies()
# 过滤掉空值
filtered_cookies = {k: v for k, v in cookies.items() if k and v}
return filtered_cookies
except Exception as e:
self.logger.error(f"获取请求Cookie失败: {e}")
return {}
def format_cookie_string(self, cookies: Dict[str, str]) -> str:
"""将Cookie字典格式化为字符串
Args:
cookies: Cookie字典
Returns:
Cookie字符串
"""
if not cookies:
return ""
return '; '.join(f"{k}={v}" for k, v in cookies.items() if k and v)
def __str__(self) -> str:
"""字符串表示"""
info = self.get_cookie_info()
return f"CookieManager(file={info['file_path']}, valid={info['is_valid']}, count={info['cookie_count']})"
def __repr__(self) -> str:
"""详细字符串表示"""
return self.__str__()
if __name__ == "__main__":
# 测试代码
import sys
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
manager = CookieManager()
print("Cookie管理器模块")
print("支持的功能:")
print("- Cookie文件读写")
print("- Cookie格式验证")
print("- Cookie有效性检查")
print("- Cookie备份和恢复")
print("- Cookie信息查看")
# 显示当前Cookie信息
info = manager.get_cookie_info()
print(f"\n当前Cookie状态: {manager}")
print(f"详细信息: {info}")
+15 -15
View File
@@ -22,9 +22,9 @@ try:
from music_api import ( from music_api import (
NeteaseAPI, APIException, QualityLevel, NeteaseAPI, APIException, QualityLevel,
url_v1, name_v1, lyric_v1, search_music, url_v1, name_v1, lyric_v1, search_music,
playlist_detail, album_detail playlist_detail, album_detail,
load_cookies,
) )
from cookie_manager import CookieManager, CookieException
from music_downloader import MusicDownloader, DownloadException, AudioFormat from music_downloader import MusicDownloader, DownloadException, AudioFormat
except ImportError as e: except ImportError as e:
print(f"导入模块失败: {e}") print(f"导入模块失败: {e}")
@@ -79,7 +79,6 @@ class MusicAPIService:
def __init__(self, config: APIConfig): def __init__(self, config: APIConfig):
self.config = config self.config = config
self.logger = self._setup_logger() self.logger = self._setup_logger()
self.cookie_manager = CookieManager()
self.netease_api = NeteaseAPI() self.netease_api = NeteaseAPI()
self.downloader = MusicDownloader() self.downloader = MusicDownloader()
@@ -117,15 +116,14 @@ class MusicAPIService:
return logger return logger
def _get_cookies(self) -> Dict[str, str]: def _get_cookies(self) -> Dict[str, str]:
"""获取Cookie""" """直接从 cookie.txt 读取并解析为 dict"""
try: try:
cookie_str = self.cookie_manager.read_cookie() cookies = load_cookies()
return self.cookie_manager.parse_cookie_string(cookie_str) if not cookies:
except CookieException as e: self.logger.warning("cookie.txt 为空或不存在,部分接口(VIP 音质)将不可用")
self.logger.warning(f"获取Cookie失败: {e}") return cookies
return {}
except Exception as e: except Exception as e:
self.logger.error(f"Cookie处理异常: {e}") self.logger.error(f"读取 cookie.txt 异常: {e}")
return {} return {}
def _extract_music_id(self, id_or_url: str) -> str: def _extract_music_id(self, id_or_url: str) -> str:
@@ -260,13 +258,15 @@ def index() -> str:
def health_check(): def health_check():
"""健康检查API""" """健康检查API"""
try: try:
# 检查Cookie状态 # 检查 Cookie 状态:直接读 cookie.txt
cookie_status = api_service.cookie_manager.is_cookie_valid() cookies = load_cookies()
cookie_status = 'valid' if cookies else 'invalid'
health_info = { health_info = {
'service': 'running', 'service': 'running',
'timestamp': int(time.time()) if 'time' in sys.modules else None, 'timestamp': int(time.time()) if 'time' in sys.modules else None,
'cookie_status': 'valid' if cookie_status else 'invalid', 'cookie_status': cookie_status,
'cookie_count': len(cookies),
'downloads_dir': str(api_service.downloads_path.absolute()), 'downloads_dir': str(api_service.downloads_path.absolute()),
'version': '2.0.0' 'version': '2.0.0'
} }
@@ -298,7 +298,7 @@ def get_song_info():
music_id = api_service._extract_music_id(song_ids or url) music_id = api_service._extract_music_id(song_ids or url)
# 验证音质参数 # 验证音质参数
valid_levels = ['standard', 'exhigh', 'lossless', 'hires', 'sky', 'jyeffect', 'jymaster'] valid_levels = ['standard', 'exhigh', 'lossless', 'hires', 'sky', 'jyeffect', 'jymaster', 'dolby']
if level not in valid_levels: if level not in valid_levels:
return APIResponse.error(f"无效的音质参数,支持: {', '.join(valid_levels)}") return APIResponse.error(f"无效的音质参数,支持: {', '.join(valid_levels)}")
@@ -500,7 +500,7 @@ def download_music_api():
return validation_error return validation_error
# 验证音质参数 # 验证音质参数
valid_qualities = ['standard', 'exhigh', 'lossless', 'hires', 'sky', 'jyeffect', 'jymaster'] valid_qualities = ['standard', 'exhigh', 'lossless', 'hires', 'sky', 'jyeffect', 'jymaster', 'dolby']
if quality not in valid_qualities: if quality not in valid_qualities:
return APIResponse.error(f"无效的音质参数,支持: {', '.join(valid_qualities)}") return APIResponse.error(f"无效的音质参数,支持: {', '.join(valid_qualities)}")
+52 -1
View File
@@ -10,6 +10,7 @@
""" """
import json import json
import os
import urllib.parse import urllib.parse
import time import time
from random import randrange from random import randrange
@@ -177,7 +178,7 @@ class NeteaseAPI:
payload = { payload = {
'ids': [song_id], 'ids': [song_id],
'level': quality, 'level': quality,
'encodeType': 'flac', 'encodeType': 'mp4' if quality == 'dolby' else 'flac',
'header': json.dumps(config), 'header': json.dumps(config),
} }
@@ -661,6 +662,56 @@ def qr_login() -> Optional[str]:
return manager.qr_login() return manager.qr_login()
# ============ Cookie 工具 ============
DEFAULT_COOKIE_FILE = "cookie.txt"
def load_cookies(path: str = DEFAULT_COOKIE_FILE) -> Dict[str, str]:
"""从 cookie.txt 直接读取并解析为 dict
支持标准 ``NMTID=xxx; _ntes_nnid=xxx; ...`` 格式。
文件不存在或解析失败返回空 dict(不抛异常,调用方决定如何处理)。
"""
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read().strip()
except OSError:
return {}
return parse_cookie_string(content)
def parse_cookie_string(cookie_string: str) -> Dict[str, str]:
"""把 ``k1=v1; k2=v2`` 格式字符串解析为 dict
支持 ``;`` 或 ``\\n`` 作为分隔符。
"""
cookies: Dict[str, str] = {}
if not cookie_string:
return cookies
cookie_string = cookie_string.strip()
if not cookie_string:
return cookies
for sep in (";", "\n"):
if sep in cookie_string:
pairs = cookie_string.split(sep)
break
else:
pairs = [cookie_string]
for pair in pairs:
pair = pair.strip()
if not pair or "=" not in pair:
continue
key, value = pair.split("=", 1)
key = key.strip()
value = value.strip()
if key and value:
cookies[key] = value
return cookies
if __name__ == "__main__": if __name__ == "__main__":
# 测试代码 # 测试代码
print("网易云音乐API模块") print("网易云音乐API模块")
+509 -383
View File
File diff suppressed because it is too large Load Diff
+150 -164
View File
@@ -3,40 +3,43 @@
提供网易云音乐二维码登录功能,包括: 提供网易云音乐二维码登录功能,包括:
- 二维码生成和显示 - 二维码生成和显示
- 登录状态检查 - 登录状态检查
- Cookie获取和保存 - Cookie 获取和保存(直接读写 cookie.txt)
- 用户友好的交互界面 - 用户友好的交互界面
""" """
import os
import sys import sys
import time import time
import shutil
import logging import logging
from typing import Optional, Dict, Any, Tuple import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple
try: try:
from music_api import QRLoginManager, APIException from music_api import QRLoginManager, APIException, load_cookies
from cookie_manager import CookieManager, CookieException
except ImportError as e: except ImportError as e:
print(f"导入模块失败: {e}") print(f"导入模块失败: {e}")
print("请确保 music_api.py 和 cookie_manager.py 文件存在且可用") print("请确保 music_api.py 文件存在且可用")
sys.exit(1) sys.exit(1)
# 判断 cookie 是否"有效":包含任一关键字段即可
IMPORTANT_COOKIE_KEYS = ("MUSIC_U", "__csrf", "NMTID")
class QRLoginClient: class QRLoginClient:
"""二维码登录客户端""" """二维码登录客户端"""
def __init__(self, cookie_file: str = "cookie.txt"): def __init__(self, cookie_file: str = "cookie.txt"):
""" """
初始化二维码登录客户端
Args: Args:
cookie_file: Cookie保存文件路径 cookie_file: Cookie 保存文件路径
""" """
self.cookie_manager = CookieManager(cookie_file) self.cookie_file = Path(cookie_file)
self.qr_manager = QRLoginManager() self.qr_manager = QRLoginManager()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
# 配置日志
if not self.logger.handlers: if not self.logger.handlers:
handler = logging.StreamHandler() handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
@@ -44,109 +47,146 @@ class QRLoginClient:
self.logger.addHandler(handler) self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
def check_existing_login(self) -> bool: # ----------- Cookie 文件直接操作 -----------
"""检查是否已有有效登录
Returns: def _read_cookie(self) -> str:
是否已登录 if not self.cookie_file.exists():
""" return ""
try: try:
if self.cookie_manager.is_cookie_valid(): return self.cookie_file.read_text(encoding="utf-8").strip()
self.logger.info("检测到有效的登录Cookie") 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 return True
else: except OSError as e:
self.logger.info("未检测到有效的登录Cookie") 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 return False
except Exception as e: except Exception as e:
self.logger.error(f"检查登录状态失败: {e}") self.logger.error(f"检查登录状态失败: {e}")
return False return False
def interactive_login(self) -> Tuple[bool, Optional[str]]: def interactive_login(self) -> Tuple[bool, Optional[str]]:
"""交互式二维码登录 """交互式二维码登录"""
Returns:
(登录是否成功, 错误信息)
"""
try: try:
print("\n=== 网易云音乐二维码登录 ===") print("\n=== 网易云音乐二维码登录 ===")
# 检查现有登录状态
if self.check_existing_login(): if self.check_existing_login():
choice = input("检测到已有有效登录,是否重新登录?(y/N): ").strip().lower() choice = input("检测到已有有效登录,是否重新登录?(y/N): ").strip().lower()
if choice not in ['y', 'yes', '是']: if choice not in ('y', 'yes', '是'):
print("使用现有登录状态") print("使用现有登录状态")
return True, None return True, None
print("\n开始二维码登录流程...") print("\n开始二维码登录流程...")
# 生成二维码
print("正在生成二维码...") print("正在生成二维码...")
qr_result = self.qr_manager.create_qr_login() qr_result = self.qr_manager.create_qr_login()
if not qr_result['success']: if not qr_result.get('success'):
error_msg = f"生成二维码失败: {qr_result.get('message', '未知错误')}" error_msg = f"生成二维码失败: {qr_result.get('message', '未知错误')}"
self.logger.error(error_msg) self.logger.error(error_msg)
return False, error_msg return False, error_msg
qr_key = qr_result['qr_key'] qr_key = qr_result['qr_key']
print(f"\n二维码已生成!") print("\n二维码已生成!")
print(f"请使用网易云音乐手机APP扫描二维码进行登录") print("请使用网易云音乐手机 APP 扫描二维码进行登录")
print(f"二维码有效期: 3分钟") print("二维码有效期: 3 分钟")
print("\n等待扫码中...") print("\n等待扫码中...")
# 轮询检查登录状态 max_attempts = 60 # 最多 5 分钟
max_attempts = 60 # 最多等待5分钟
attempt = 0 attempt = 0
while attempt < max_attempts: while attempt < max_attempts:
try: try:
# 检查登录状态
status_result = self.qr_manager.check_qr_login(qr_key) status_result = self.qr_manager.check_qr_login(qr_key)
if status_result['success']: if status_result.get('success'):
if status_result['status'] == 'success': status = status_result.get('status')
# 登录成功 if status == 'success':
cookie = status_result.get('cookie', '') cookie = status_result.get('cookie', '')
if cookie: if not cookie:
# 保存Cookie return False, "登录成功但未获取到 Cookie"
success = self.save_cookie(cookie) if self.save_cookie(cookie):
if success: print("\n✅ 登录成功!Cookie 已保存")
print("\n✅ 登录成功!Cookie已保存")
return True, None return True, None
else: return False, "登录成功但 Cookie 保存失败"
error_msg = "登录成功但Cookie保存失败"
self.logger.error(error_msg)
return False, error_msg
else:
error_msg = "登录成功但未获取到Cookie"
self.logger.error(error_msg)
return False, error_msg
elif status_result['status'] == 'waiting': if status == 'waiting' and attempt % 10 == 0:
# 等待扫码
if attempt % 10 == 0: # 每10次显示一次提示
print(f"等待扫码中... ({attempt + 1}/{max_attempts})") print(f"等待扫码中... ({attempt + 1}/{max_attempts})")
elif status == 'scanned':
elif status_result['status'] == 'scanned':
# 已扫码,等待确认
print("二维码已扫描,请在手机上确认登录") print("二维码已扫描,请在手机上确认登录")
elif status == 'expired':
elif status_result['status'] == 'expired': print("\n❌ 二维码已过期,请重新尝试")
# 二维码过期 return False, "二维码已过期"
error_msg = "二维码已过期,请重新尝试" elif status == 'error':
print(f"\n❌ {error_msg}") msg = status_result.get('message', '未知错误')
return False, error_msg print(f"\n❌ 登录失败: {msg}")
return False, f"登录失败: {msg}"
elif status_result['status'] == 'error':
# 登录错误
error_msg = f"登录失败: {status_result.get('message', '未知错误')}"
print(f"\n❌ {error_msg}")
return False, error_msg
else: else:
self.logger.warning(f"检查登录状态失败: {status_result.get('message', '未知错误')}") self.logger.warning(f"检查登录状态失败: {status_result.get('message', '未知错误')}")
# 等待5秒后重试
time.sleep(5) time.sleep(5)
attempt += 1 attempt += 1
@@ -158,118 +198,76 @@ class QRLoginClient:
time.sleep(5) time.sleep(5)
attempt += 1 attempt += 1
# 超时 print("\n❌ 登录超时,请重新尝试")
error_msg = "登录超时,请重新尝试" return False, "登录超时"
print(f"\n❌ {error_msg}")
return False, error_msg
except APIException as e: except APIException as e:
error_msg = f"API调用失败: {e}" self.logger.error(f"API 调用失败: {e}")
self.logger.error(error_msg) return False, f"API 调用失败: {e}"
return False, error_msg
except Exception as e: except Exception as e:
error_msg = f"登录过程中发生未知错误: {e}" self.logger.error(f"登录过程中发生未知错误: {e}")
self.logger.error(error_msg) return False, f"登录过程中发生未知错误: {e}"
return False, error_msg
def save_cookie(self, cookie: str) -> bool: def save_cookie(self, cookie: str) -> bool:
"""保存Cookie到文件 """保存 Cookie 到文件"""
Args:
cookie: Cookie字符串
Returns:
是否保存成功
"""
try: try:
# 备份现有Cookie(如果存在) if self.cookie_file.exists():
if self.cookie_manager.cookie_file.exists(): backup = self._backup_cookie()
try: if backup:
backup_path = self.cookie_manager.backup_cookie() self.logger.info(f"已备份现有 Cookie 到: {backup}")
self.logger.info(f"已备份现有Cookie到: {backup_path}")
except Exception as e:
self.logger.warning(f"备份Cookie失败: {e}")
# 保存新Cookie if not self._write_cookie(cookie):
success = self.cookie_manager.write_cookie(cookie) return False
if success: if self._is_cookie_valid():
# 验证保存的Cookie self.logger.info("Cookie 保存并验证成功")
if self.cookie_manager.is_cookie_valid():
self.logger.info("Cookie保存并验证成功")
return True return True
else:
self.logger.warning("Cookie保存成功但验证失败")
return False
else:
self.logger.error("Cookie保存失败")
return False
except CookieException as e: self.logger.warning("Cookie 保存成功但验证失败(缺少关键字段)")
self.logger.error(f"Cookie操作失败: {e}")
return False return False
except Exception as e: except Exception as e:
self.logger.error(f"保存Cookie时发生错误: {e}") self.logger.error(f"保存 Cookie 时发生错误: {e}")
return False return False
def show_login_info(self) -> None: def show_login_info(self) -> None:
"""显示登录信息""" """显示登录信息"""
try: try:
info = self.cookie_manager.get_cookie_info() info = self._get_cookie_info()
print("\n=== 登录状态信息 ===") print("\n=== 登录状态信息 ===")
print(f"Cookie文件: {info['file_path']}") print(f"Cookie 文件: {info['file_path']}")
print(f"文件存在: {'是' if info['file_exists'] else '否'}") print(f"文件存在: {'是' if info['file_exists'] else '否'}")
print(f"Cookie数量: {info['cookie_count']}") print(f"Cookie 数量: {info['cookie_count']}")
print(f"登录状态: {'有效' if info['is_valid'] else '无效'}") print(f"登录状态: {'有效' if info['is_valid'] else '无效'}")
if info.get("last_modified"):
if info.get('last_modified'):
print(f"最后更新: {info['last_modified']}") print(f"最后更新: {info['last_modified']}")
if info["is_valid"]:
if info['is_valid']: present = info.get("important_cookies_present", [])
present_cookies = info.get('important_cookies_present', []) print(f"重要 Cookie: {', '.join(present)}")
print(f"重要Cookie: {', '.join(present_cookies)}")
else: else:
missing_cookies = info.get('missing_important_cookies', []) missing = info.get("missing_important_cookies", [])
if missing_cookies: if missing:
print(f"缺少Cookie: {', '.join(missing_cookies)}") print(f"缺少 Cookie: {', '.join(missing)}")
except Exception as e: except Exception as e:
print(f"获取登录信息失败: {e}") print(f"获取登录信息失败: {e}")
def logout(self) -> bool: def logout(self) -> bool:
"""登出(清除Cookie) """登出(清除 Cookie)"""
Returns:
是否登出成功
"""
try: try:
# 备份Cookie if self.cookie_file.exists():
if self.cookie_manager.cookie_file.exists(): backup = self._backup_cookie("logout")
try: if backup:
backup_path = self.cookie_manager.backup_cookie("logout") print(f"Cookie 已备份到: {backup}")
print(f"Cookie已备份到: {backup_path}") if self._clear_cookie():
except Exception as e:
self.logger.warning(f"备份Cookie失败: {e}")
# 清除Cookie
success = self.cookie_manager.clear_cookie()
if success:
print("已成功登出") print("已成功登出")
return True return True
else:
print("登出失败") print("登出失败")
return False return False
except Exception as e: except Exception as e:
print(f"登出时发生错误: {e}") print(f"登出时发生错误: {e}")
return False return False
def main(): def main():
"""主函数"""
# 配置日志
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
@@ -277,40 +275,32 @@ def main():
client = QRLoginClient() client = QRLoginClient()
# 解析命令行参数
if len(sys.argv) > 1: if len(sys.argv) > 1:
command = sys.argv[1].lower() command = sys.argv[1].lower()
if command == 'login': if command == 'login':
# 执行登录
success, error = client.interactive_login() success, error = client.interactive_login()
if success: if success:
print("\n登录完成!") print("\n登录完成!")
client.show_login_info() client.show_login_info()
sys.exit(0) sys.exit(0)
else:
print(f"\n登录失败: {error}") print(f"\n登录失败: {error}")
sys.exit(1) sys.exit(1)
elif command == 'status' or command == 'info': elif command in ('status', 'info'):
# 显示登录状态
client.show_login_info() client.show_login_info()
sys.exit(0) sys.exit(0)
elif command == 'logout': elif command == 'logout':
# 登出 sys.exit(0 if client.logout() else 1)
success = client.logout()
sys.exit(0 if success else 1)
elif command == 'help' or command == '-h' or command == '--help': elif command in ('help', '-h', '--help'):
# 显示帮助
print("网易云音乐二维码登录工具") print("网易云音乐二维码登录工具")
print("\n用法:") print("\n用法: python qr_login.py [命令]")
print(" python qr_login.py [命令]")
print("\n命令:") print("\n命令:")
print(" login - 执行二维码登录") print(" login - 执行二维码登录")
print(" status - 显示登录状态") print(" status - 显示登录状态")
print(" logout - 登出(清除Cookie)") print(" logout - 登出(清除 Cookie)")
print(" help - 显示此帮助信息") print(" help - 显示此帮助信息")
print("\n如果不提供命令,将进入交互模式") print("\n如果不提供命令,将进入交互模式")
sys.exit(0) sys.exit(0)
@@ -338,17 +328,13 @@ def main():
client.show_login_info() client.show_login_info()
else: else:
print(f"\n登录失败: {error}") print(f"\n登录失败: {error}")
elif choice == '2': elif choice == '2':
client.show_login_info() client.show_login_info()
elif choice == '3': elif choice == '3':
client.logout() client.logout()
elif choice == '4': elif choice == '4':
print("再见!") print("再见!")
break break
else: else:
print("无效选择,请重试") print("无效选择,请重试")
BIN
View File
Binary file not shown.
+1
View File
@@ -129,6 +129,7 @@
<option value="sky">沉浸环绕声</option> <option value="sky">沉浸环绕声</option>
<option value="jyeffect">高清环绕声</option> <option value="jyeffect">高清环绕声</option>
<option value="jymaster">超清母带</option> <option value="jymaster">超清母带</option>
<option value="dolby">杜比全景声</option>
</select> </select>
</div> </div>
<div class="text-center"> <div class="text-center">
+2 -3
View File
@@ -417,10 +417,9 @@ tail -f music_api.log
``` ```
Netease_url/ Netease_url/
├── main.py # 主程序入口 ├── main.py # 主程序入口
├── music_api.py # 音乐API核心模块 ├── music_api.py # 音乐API核心模块(含 load_cookies 工具)
├── music_downloader.py # 音乐下载模块 ├── music_downloader.py # 音乐下载模块
├── cookie_manager.py # Cookie管理模块 ├── qr_login.py # 二维码登录模块(直接读写 cookie.txt)
├── qr_login.py # 二维码登录模块
├── templates/ ├── templates/
│ └── index.html # Web界面模板 │ └── index.html # Web界面模板
├── downloads/ # 下载文件目录 ├── downloads/ # 下载文件目录