# -*- coding: utf-8 -*- """数据模型、异常与格式常量。 (自 music_downloader.py 拆出,行为与原实现一致。) """ from __future__ import annotations from dataclasses import dataclass from enum import Enum # ============ 类型定义 ============ class QualityLevel(Enum): """音质等级枚举""" STANDARD = "standard" EXHIGH = "exhigh" LOSSLESS = "lossless" HIRES = "hires" SKY = "sky" JYEFFECT = "jyeffect" JYMASTER = "jymaster" DOLBY = "dolby" @dataclass class MusicInfo: """音乐信息数据类""" id: int name: str artists: str album: str pic_url: str duration: int track_number: int download_url: str file_type: str file_size: int quality: str lyric: str = "" tlyric: str = "" publish_year: int | None = None @dataclass class DownloadResult: """下载结果数据类""" success: bool file_path: str | None = None file_size: int = 0 error_message: str = "" music_info: MusicInfo | None = None class DownloadException(Exception): """下载异常类""" pass class DownloadCanceled(DownloadException): """下载被用户取消""" pass class _RetryFromScratch(Exception): """内部信号:断点续传起点失效(HTTP 416 / Content-Range 错位),清除 .part 后从头重试。 刻意**不**继承 DownloadException 或 UpstreamError:外层 ``except DownloadException`` 的语义是"确定性失败,不重试",本信号恰恰要求 从头重来;``except UpstreamError`` 的语义是"网络失败,保留 .part 续传", 而本信号已经把残片清掉了。两条语义都不是它,故独立成类、单独先于二者捕获。 """ pass # ============ 工具映射表 ============ # API 返回的 type -> 文件扩展名 API_TYPE_TO_EXT = { "mp3": ".mp3", "flac": ".flac", "m4a": ".m4a", "mp4": ".mp4", "ogg": ".ogg", "opus": ".opus", } # 支持元数据写入的扩展名(tagging.py 按此表分派到各容器的标签写入器) METADATA_FORMATS = {".mp3", ".flac", ".m4a", ".mp4", ".ogg", ".opus"}