chore: move enums and objects to single file
This commit is contained in:
parent
1ca227b8be
commit
3c8a46d039
@ -6,10 +6,12 @@ from click import argument, command, confirm, option
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
|
|
||||||
from ncmlyrics.error import UnsupportLinkError
|
from .api import NCMApi
|
||||||
|
from .enum import LinkType
|
||||||
from .api import NCMApi, NCMTrack
|
from .error import UnsupportedLinkError
|
||||||
from .util import Link, LinkType, parseLink, pickOutput
|
from .lrc import Lrc
|
||||||
|
from .object import NCMTrack
|
||||||
|
from .util import Link, parseLink, pickOutput
|
||||||
|
|
||||||
NCMLyricsAppTheme = Theme(
|
NCMLyricsAppTheme = Theme(
|
||||||
{
|
{
|
||||||
@ -136,7 +138,7 @@ def main(outputs: list[Path], exist: bool, overwrite: bool, quiet: bool, links:
|
|||||||
if ncmlyrics.isPureMusic:
|
if ncmlyrics.isPureMusic:
|
||||||
console.print(f"曲目 {track.name} 为纯音乐, 跳过此曲目")
|
console.print(f"曲目 {track.name} 为纯音乐, 跳过此曲目")
|
||||||
else:
|
else:
|
||||||
ncmlyrics.lrc().saveAs(path)
|
Lrc.fromNCMLyrics(ncmlyrics).saveAs(path)
|
||||||
console.print(f"--> {str(path)}")
|
console.print(f"--> {str(path)}")
|
||||||
|
|
||||||
api.saveCookies()
|
api.saveCookies()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from http.cookiejar import LoadError, MozillaCookieJar
|
from http.cookiejar import LoadError, MozillaCookieJar
|
||||||
from json import dumps as dumpJson, JSONDecodeError
|
from json import dumps as dumpJson
|
||||||
from typing import Any, Iterable, Self
|
from typing import Iterable
|
||||||
|
|
||||||
from httpx import Client as HttpXClient
|
from httpx import Client as HttpXClient
|
||||||
from httpx import Request as HttpXRequest
|
from httpx import Request as HttpXRequest
|
||||||
@ -9,12 +9,10 @@ from httpx import Response as HttpXResponse
|
|||||||
|
|
||||||
from .constant import CONFIG_API_DETAIL_TRACK_PER_REQUEST, NCM_API_BASE_URL, PLATFORM
|
from .constant import CONFIG_API_DETAIL_TRACK_PER_REQUEST, NCM_API_BASE_URL, PLATFORM
|
||||||
from .error import (
|
from .error import (
|
||||||
NCMApiResponseParseError,
|
|
||||||
NCMApiRequestError,
|
NCMApiRequestError,
|
||||||
NCMApiRetryLimitExceededError,
|
NCMApiRetryLimitExceededError,
|
||||||
UnsupportedPureMusicTrackError,
|
|
||||||
)
|
)
|
||||||
from .lrc import Lrc, LrcType
|
from .object import NCMAlbum, NCMLyrics, NCMPlaylist, NCMTrack
|
||||||
|
|
||||||
REQUEST_HEADERS = {
|
REQUEST_HEADERS = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@ -24,171 +22,6 @@ REQUEST_HEADERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NCMTrack:
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
artists: list[str]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromApi(cls, response: HttpXResponse) -> list[Self]:
|
|
||||||
try:
|
|
||||||
data: dict = response.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应")
|
|
||||||
|
|
||||||
if data.get("code") != 200:
|
|
||||||
raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}")
|
|
||||||
|
|
||||||
data = data.get("songs")
|
|
||||||
if data is None:
|
|
||||||
raise NCMApiResponseParseError("不存在单曲对应的结构")
|
|
||||||
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for track in data:
|
|
||||||
result.append(cls.fromData(track))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromData(cls, data: dict) -> Self:
|
|
||||||
try:
|
|
||||||
return cls(
|
|
||||||
id=data["id"],
|
|
||||||
name=data["name"],
|
|
||||||
artists=[artist["name"] for artist in data["ar"]],
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
raise NCMApiResponseParseError(f"需要的键不存在: {e}")
|
|
||||||
|
|
||||||
def link(self) -> str:
|
|
||||||
return f"https://music.163.com/song?id={self.id}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NCMAlbum:
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
tracks: list[NCMTrack]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromApi(cls, response: HttpXResponse) -> Self:
|
|
||||||
try:
|
|
||||||
data: dict = response.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应")
|
|
||||||
|
|
||||||
if data.get("code") != 200:
|
|
||||||
raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}")
|
|
||||||
|
|
||||||
album = data.get("album")
|
|
||||||
if album is None:
|
|
||||||
raise NCMApiResponseParseError("不存在专辑对应的结构")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return cls(
|
|
||||||
id=album["id"],
|
|
||||||
name=album["name"],
|
|
||||||
tracks=[NCMTrack.fromData(track) for track in data["songs"]],
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
raise NCMApiResponseParseError(f"需要的键不存在: {e}")
|
|
||||||
|
|
||||||
def link(self) -> str:
|
|
||||||
return f"https://music.163.com/album?id={self.id}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NCMPlaylist:
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
tracks: list[NCMTrack]
|
|
||||||
trackIds: list[int]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromApi(cls, response: HttpXResponse) -> Self:
|
|
||||||
try:
|
|
||||||
data: dict = response.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应")
|
|
||||||
|
|
||||||
if data.get("code") != 200:
|
|
||||||
raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}")
|
|
||||||
|
|
||||||
playlist = data.get("playlist")
|
|
||||||
if playlist is None:
|
|
||||||
raise NCMApiResponseParseError("不存在歌单对应的结构")
|
|
||||||
|
|
||||||
try:
|
|
||||||
tracks: list[NCMTrack] = []
|
|
||||||
trackIds: list[int] = [track["id"] for track in playlist["trackIds"]]
|
|
||||||
|
|
||||||
for track in playlist["tracks"]:
|
|
||||||
parsedTrack = NCMTrack.fromData(track)
|
|
||||||
trackIds.remove(parsedTrack.id)
|
|
||||||
tracks.append(parsedTrack)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
id=playlist["id"],
|
|
||||||
name=playlist["name"],
|
|
||||||
tracks=tracks,
|
|
||||||
trackIds=trackIds,
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
raise NCMApiResponseParseError(f"需要的键不存在: {e}")
|
|
||||||
|
|
||||||
def link(self) -> str:
|
|
||||||
return f"https://music.163.com/playlist?id={self.id}"
|
|
||||||
|
|
||||||
def fillDetailsOfTracks(self, api) -> None:
|
|
||||||
self.tracks.extend(api.getDetailsForTracks(self.trackIds))
|
|
||||||
self.trackIds.clear()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NCMLyrics:
|
|
||||||
id: int | None
|
|
||||||
isPureMusic: bool
|
|
||||||
data: Any | None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromApi(cls, response: HttpXResponse) -> Self:
|
|
||||||
try:
|
|
||||||
data: dict = response.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应")
|
|
||||||
|
|
||||||
if data.get("code") != 200:
|
|
||||||
raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}")
|
|
||||||
|
|
||||||
if data.get("pureMusic") is True:
|
|
||||||
return cls(id=None, isPureMusic=True, data=None)
|
|
||||||
|
|
||||||
return cls(id=None, isPureMusic=False, data=data)
|
|
||||||
|
|
||||||
def withId(self, id: int) -> Self:
|
|
||||||
self.id = id
|
|
||||||
return self
|
|
||||||
|
|
||||||
def lrc(self) -> Lrc:
|
|
||||||
if self.isPureMusic:
|
|
||||||
raise UnsupportedPureMusicTrackError
|
|
||||||
|
|
||||||
result = Lrc()
|
|
||||||
|
|
||||||
for lrcType in LrcType:
|
|
||||||
try:
|
|
||||||
lrcStr = self.data[lrcType.value]["lyric"]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if lrcStr != "":
|
|
||||||
result.serializeLyricString(lrcType, lrcStr)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class NCMApi:
|
class NCMApi:
|
||||||
def __init__(self, http2: bool = True) -> None:
|
def __init__(self, http2: bool = True) -> None:
|
||||||
self._cookiePath = PLATFORM.user_config_path / "cookies.txt"
|
self._cookiePath = PLATFORM.user_config_path / "cookies.txt"
|
||||||
|
34
src/ncmlyrics/enum.py
Normal file
34
src/ncmlyrics/enum.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
|
__all__ = ["LrcType", "LrcMetaType", "LinkType"]
|
||||||
|
|
||||||
|
|
||||||
|
class LrcType(Enum):
|
||||||
|
Origin = auto()
|
||||||
|
Translation = auto()
|
||||||
|
Romaji = auto()
|
||||||
|
|
||||||
|
def preety(self) -> str:
|
||||||
|
match self:
|
||||||
|
case LrcType.Origin:
|
||||||
|
return "源"
|
||||||
|
case LrcType.Translation:
|
||||||
|
return "译"
|
||||||
|
case LrcType.Romaji:
|
||||||
|
return "音"
|
||||||
|
|
||||||
|
|
||||||
|
class LrcMetaType(Enum):
|
||||||
|
Title = "ti"
|
||||||
|
Artist = "ar"
|
||||||
|
Album = "al"
|
||||||
|
Author = "au"
|
||||||
|
Length = "length"
|
||||||
|
LrcAuthor = "by"
|
||||||
|
Offset = "offset"
|
||||||
|
|
||||||
|
|
||||||
|
class LinkType(Enum):
|
||||||
|
Song = auto()
|
||||||
|
Album = auto()
|
||||||
|
Playlist = auto()
|
@ -13,14 +13,14 @@ class NCMApiRequestError(NCMApiError, RequestError):
|
|||||||
"""请求网易云音乐 API 时出现错误"""
|
"""请求网易云音乐 API 时出现错误"""
|
||||||
|
|
||||||
|
|
||||||
class NCMApiResponseParseError(NCMApiError):
|
|
||||||
"""解析网易云音乐 API 返回的数据时出现错误"""
|
|
||||||
|
|
||||||
|
|
||||||
class NCMApiRetryLimitExceededError(NCMApiError):
|
class NCMApiRetryLimitExceededError(NCMApiError):
|
||||||
"""请求网易云音乐 API 时错误次数超过重试次数上限"""
|
"""请求网易云音乐 API 时错误次数超过重试次数上限"""
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectParseError(NCMLyricsAppError):
|
||||||
|
"""解析网易云音乐 API 返回的数据时出现错误"""
|
||||||
|
|
||||||
|
|
||||||
class ParseLinkError(NCMLyricsAppError):
|
class ParseLinkError(NCMLyricsAppError):
|
||||||
"""无法解析此分享链接"""
|
"""无法解析此分享链接"""
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
from enum import Enum
|
from json import JSONDecodeError
|
||||||
|
from json import loads as loadJson
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from re import Match
|
from re import Match
|
||||||
from re import compile as reCompile
|
from re import compile as reCompile
|
||||||
from typing import Generator
|
from typing import Generator, Iterable, Self
|
||||||
|
|
||||||
from .constant import CONFIG_LRC_AUTO_MERGE, CONFIG_LRC_AUTO_MERGE_OFFSET
|
from .constant import CONFIG_LRC_AUTO_MERGE, CONFIG_LRC_AUTO_MERGE_OFFSET
|
||||||
|
from .enum import LrcMetaType, LrcType
|
||||||
|
from .error import UnsupportedPureMusicTrackError
|
||||||
|
from .object import NCMLyrics
|
||||||
|
|
||||||
|
__all__ = ["LrcType", "LrcMetaType", "Lrc"]
|
||||||
|
|
||||||
LRC_RE_COMMIT = reCompile(r"^\s*#")
|
LRC_RE_COMMIT = reCompile(r"^\s*#")
|
||||||
LRC_RE_META = reCompile(r"^\s*\[(?P<type>ti|ar|al|au|length|by|offset):\s*(?P<content>.+?)\s*\]\s*$")
|
LRC_RE_META = reCompile(r"^\s*\[(?P<type>ti|ar|al|au|length|by|offset):\s*(?P<content>.+?)\s*\]\s*$")
|
||||||
@ -13,60 +19,55 @@ LRC_RE_LYRIC = reCompile(r"^\s*(?P<timelabels>(?:\s*\[\d{1,2}:\d{1,2}(?:\.\d{1,3
|
|||||||
LRC_RE_LYRIC_TIMELABEL = reCompile(r"\[(?P<minutes>\d{1,2}):(?P<seconds>\d{1,2}(?:\.\d{1,3})?)\]")
|
LRC_RE_LYRIC_TIMELABEL = reCompile(r"\[(?P<minutes>\d{1,2}):(?P<seconds>\d{1,2}(?:\.\d{1,3})?)\]")
|
||||||
|
|
||||||
|
|
||||||
class LrcType(Enum):
|
|
||||||
Origin = "lrc"
|
|
||||||
Translation = "tlyric"
|
|
||||||
Romaji = "romalrc"
|
|
||||||
|
|
||||||
def preety(self) -> str:
|
|
||||||
match self:
|
|
||||||
case LrcType.Origin:
|
|
||||||
return "源"
|
|
||||||
case LrcType.Translation:
|
|
||||||
return "译"
|
|
||||||
case LrcType.Romaji:
|
|
||||||
return "音"
|
|
||||||
|
|
||||||
|
|
||||||
class LrcMetaType(Enum):
|
|
||||||
Title = "ti"
|
|
||||||
Artist = "ar"
|
|
||||||
Album = "al"
|
|
||||||
Author = "au"
|
|
||||||
Length = "length"
|
|
||||||
LrcAuthor = "by"
|
|
||||||
Offset = "offset"
|
|
||||||
|
|
||||||
|
|
||||||
class Lrc:
|
class Lrc:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# metaType: lrcType: metaContent
|
# metaType: lrcType: metaContent
|
||||||
self.metadata: dict[LrcMetaType, dict[LrcType, str]] = {}
|
self.metadata: dict[LrcMetaType, dict[LrcType, str]] = {}
|
||||||
|
|
||||||
# timestamp: lrcType: lrcContent
|
# timestamp: lrcType/String: lrcContent
|
||||||
self.lyrics: dict[int, dict[LrcType, str]] = {}
|
self.lyrics: dict[int, dict[LrcType | str, str]] = {}
|
||||||
|
|
||||||
def serializeLyricString(self, lrcType: LrcType, lrcStr: str) -> None:
|
@classmethod
|
||||||
for line in lrcStr.splitlines():
|
def fromNCMLyrics(cls, lyrics: NCMLyrics) -> Self:
|
||||||
# Skip commit lines
|
if lyrics.isPureMusic:
|
||||||
if LRC_RE_COMMIT.match(line) is not None:
|
raise UnsupportedPureMusicTrackError
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip NCM spical metadata lines
|
result = cls()
|
||||||
if LRC_RE_META_NCM_SPICAL.match(line) is not None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
matchedMetaDataRow = LRC_RE_META.match(line)
|
for lrcType in LrcType:
|
||||||
if matchedMetaDataRow is not None:
|
lrcStr = lyrics.get(lrcType)
|
||||||
self.appendMatchedMetaDataRow(lrcType, matchedMetaDataRow)
|
if lrcStr:
|
||||||
continue
|
result.serializeLyricFile(lrcType, lrcStr)
|
||||||
|
|
||||||
matchedLyricRow = LRC_RE_LYRIC.match(line)
|
return result
|
||||||
if matchedLyricRow is not None:
|
|
||||||
self.appendMatchedLyricRow(lrcType, matchedLyricRow)
|
|
||||||
continue
|
|
||||||
|
|
||||||
def appendLyric(self, lrcType: LrcType, timestamps: list[int], lyric: str):
|
def serializeLyricFile(self, lrcType: LrcType, lrcFile: str) -> None:
|
||||||
|
self.serializeLyricRows(lrcType, lrcFile.splitlines())
|
||||||
|
|
||||||
|
def serializeLyricRows(self, lrcType: LrcType, lrcRows: Iterable[str]) -> None:
|
||||||
|
for row in lrcRows:
|
||||||
|
self.serializeLyricRow(lrcType, row)
|
||||||
|
|
||||||
|
def serializeLyricRow(self, lrcType: LrcType, lrcRow: str) -> None:
|
||||||
|
# Skip commit lines
|
||||||
|
if LRC_RE_COMMIT.match(lrcRow) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip NCM spical metadata lines
|
||||||
|
if LRC_RE_META_NCM_SPICAL.match(lrcRow) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
matchedMetaDataRow = LRC_RE_META.match(lrcRow)
|
||||||
|
if matchedMetaDataRow is not None:
|
||||||
|
self.appendMatchedMetaDataRow(lrcType, matchedMetaDataRow)
|
||||||
|
return
|
||||||
|
|
||||||
|
matchedLyricRow = LRC_RE_LYRIC.match(lrcRow)
|
||||||
|
if matchedLyricRow is not None:
|
||||||
|
self.appendMatchedLyricRow(lrcType, matchedLyricRow)
|
||||||
|
return
|
||||||
|
|
||||||
|
def appendLyric(self, lrcType: LrcType, timestamps: Iterable[int], lyric: str):
|
||||||
for timestamp in timestamps:
|
for timestamp in timestamps:
|
||||||
if timestamp in self.lyrics:
|
if timestamp in self.lyrics:
|
||||||
self.lyrics[timestamp][lrcType] = lyric
|
self.lyrics[timestamp][lrcType] = lyric
|
||||||
|
181
src/ncmlyrics/object.py
Normal file
181
src/ncmlyrics/object.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from json import JSONDecodeError
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
from .enum import LrcType
|
||||||
|
from .error import ObjectParseError
|
||||||
|
|
||||||
|
__all__ = ["NCMTrack", "NCMAlbum", "NCMPlaylist", "NCMLyrics"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NCMTrack:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
artists: list[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromApi(cls, response: Response) -> list[Self]:
|
||||||
|
try:
|
||||||
|
data: dict = response.json()
|
||||||
|
except JSONDecodeError:
|
||||||
|
raise ObjectParseError("无法以预期的 Json 格式解析响应")
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
if data.get("code") != 200:
|
||||||
|
raise ObjectParseError(f"响应码不为 200: {data["code"]}")
|
||||||
|
|
||||||
|
data = data.get("songs")
|
||||||
|
if data is None:
|
||||||
|
raise ObjectParseError("不存在单曲对应的结构")
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for track in data:
|
||||||
|
result.append(cls.fromData(track))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromData(cls, data: dict) -> Self:
|
||||||
|
try:
|
||||||
|
return cls(
|
||||||
|
id=data["id"],
|
||||||
|
name=data["name"],
|
||||||
|
artists=[artist["name"] for artist in data["ar"]],
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ObjectParseError(f"需要的键不存在: {e}")
|
||||||
|
|
||||||
|
def link(self) -> str:
|
||||||
|
return f"https://music.163.com/song?id={self.id}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NCMAlbum:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tracks: list[NCMTrack]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromApi(cls, response: Response) -> Self:
|
||||||
|
try:
|
||||||
|
data: dict = response.json()
|
||||||
|
except JSONDecodeError:
|
||||||
|
raise ObjectParseError("无法以预期的 Json 格式解析响应")
|
||||||
|
|
||||||
|
if data.get("code") != 200:
|
||||||
|
raise ObjectParseError(f"响应码不为 200: {data["code"]}")
|
||||||
|
|
||||||
|
album = data.get("album")
|
||||||
|
if album is None:
|
||||||
|
raise ObjectParseError("不存在专辑对应的结构")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return cls(
|
||||||
|
id=album["id"],
|
||||||
|
name=album["name"],
|
||||||
|
tracks=[NCMTrack.fromData(track) for track in data["songs"]],
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ObjectParseError(f"需要的键不存在: {e}")
|
||||||
|
|
||||||
|
def link(self) -> str:
|
||||||
|
return f"https://music.163.com/album?id={self.id}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NCMPlaylist:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tracks: list[NCMTrack]
|
||||||
|
trackIds: list[int]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromApi(cls, response: Response) -> Self:
|
||||||
|
try:
|
||||||
|
data: dict = response.json()
|
||||||
|
except JSONDecodeError:
|
||||||
|
raise ObjectParseError("无法以预期的 Json 格式解析响应")
|
||||||
|
|
||||||
|
if data.get("code") != 200:
|
||||||
|
raise ObjectParseError(f"响应码不为 200: {data["code"]}")
|
||||||
|
|
||||||
|
playlist = data.get("playlist")
|
||||||
|
if playlist is None:
|
||||||
|
raise ObjectParseError("不存在歌单对应的结构")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tracks: list[NCMTrack] = []
|
||||||
|
trackIds: list[int] = [track["id"] for track in playlist["trackIds"]]
|
||||||
|
|
||||||
|
for track in playlist["tracks"]:
|
||||||
|
parsedTrack = NCMTrack.fromData(track)
|
||||||
|
trackIds.remove(parsedTrack.id)
|
||||||
|
tracks.append(parsedTrack)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=playlist["id"],
|
||||||
|
name=playlist["name"],
|
||||||
|
tracks=tracks,
|
||||||
|
trackIds=trackIds,
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ObjectParseError(f"需要的键不存在: {e}")
|
||||||
|
|
||||||
|
def link(self) -> str:
|
||||||
|
return f"https://music.163.com/playlist?id={self.id}"
|
||||||
|
|
||||||
|
def fillDetailsOfTracks(self, api) -> None:
|
||||||
|
self.tracks.extend(api.getDetailsForTracks(self.trackIds))
|
||||||
|
self.trackIds.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NCMLyrics:
|
||||||
|
id: int | None
|
||||||
|
isPureMusic: bool
|
||||||
|
lyrics: dict[LrcType, str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromApi(cls, response: Response) -> Self:
|
||||||
|
try:
|
||||||
|
data: dict = response.json()
|
||||||
|
except JSONDecodeError:
|
||||||
|
raise ObjectParseError("无法以预期的 Json 格式解析响应")
|
||||||
|
|
||||||
|
if data.get("code") != 200:
|
||||||
|
raise ObjectParseError(f"响应码不为 200: {data["code"]}")
|
||||||
|
|
||||||
|
lyrics: dict[LrcType, str] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
lyrics[LrcType.Origin] = data["lrc"]["lyric"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
lyrics[LrcType.Translation] = data["tlyric"]["lyric"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
lyrics[LrcType.Romaji] = data["romalrc"]["lyric"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=None,
|
||||||
|
isPureMusic=data.get("pureMusic", False),
|
||||||
|
lyrics=lyrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
def withId(self, id: int) -> Self:
|
||||||
|
self.id = id
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get(self, type: LrcType) -> str | None:
|
||||||
|
return self.lyrics.get(type, None)
|
@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, auto
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from re import compile as reCompile
|
from re import compile as reCompile
|
||||||
from urllib.parse import parse_qs as parseQuery
|
from urllib.parse import parse_qs as parseQuery
|
||||||
@ -7,20 +6,15 @@ from urllib.parse import urlparse as parseUrl
|
|||||||
|
|
||||||
from httpx import get as httpGet
|
from httpx import get as httpGet
|
||||||
|
|
||||||
from .api import NCMTrack
|
from .enum import LinkType
|
||||||
from .error import ParseLinkError, UnsupportLinkError
|
from .error import ParseLinkError, UnsupportedLinkError
|
||||||
|
from .object import NCMTrack
|
||||||
|
|
||||||
RE_ANDROID_ALBUM_SHARE_LINK_PATH = reCompile(r"^/album/(?P<id>\d*)/?$")
|
RE_ANDROID_ALBUM_SHARE_LINK_PATH = reCompile(r"^/album/(?P<id>\d*)/?$")
|
||||||
RE_SAFE_FILENAME = reCompile(r"\*{2,}")
|
RE_SAFE_FILENAME = reCompile(r"\*{2,}")
|
||||||
TRANSLATER_SAFE_FILENAME = str.maketrans({i: 0x2A for i in ("<", ">", ":", '"', "/", "\\", "|", "?")})
|
TRANSLATER_SAFE_FILENAME = str.maketrans({i: 0x2A for i in ("<", ">", ":", '"', "/", "\\", "|", "?")})
|
||||||
|
|
||||||
|
|
||||||
class LinkType(Enum):
|
|
||||||
Song = auto()
|
|
||||||
Album = auto()
|
|
||||||
Playlist = auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Link:
|
class Link:
|
||||||
type: LinkType
|
type: LinkType
|
||||||
|
Loading…
x
Reference in New Issue
Block a user