chore: move enums and objects to single file

This commit is contained in:
Puqns67 2024-10-15 20:18:48 +08:00
parent 1ca227b8be
commit 3c8a46d039
Signed by: Puqns67
GPG Key ID: 9669DF042554F536
7 changed files with 279 additions and 234 deletions

View File

@ -6,10 +6,12 @@ from click import argument, command, confirm, option
from rich.console import Console
from rich.theme import Theme
from ncmlyrics.error import UnsupportLinkError
from .api import NCMApi, NCMTrack
from .util import Link, LinkType, parseLink, pickOutput
from .api import NCMApi
from .enum import LinkType
from .error import UnsupportedLinkError
from .lrc import Lrc
from .object import NCMTrack
from .util import Link, parseLink, pickOutput
NCMLyricsAppTheme = Theme(
{
@ -136,7 +138,7 @@ def main(outputs: list[Path], exist: bool, overwrite: bool, quiet: bool, links:
if ncmlyrics.isPureMusic:
console.print(f"曲目 {track.name} 为纯音乐, 跳过此曲目")
else:
ncmlyrics.lrc().saveAs(path)
Lrc.fromNCMLyrics(ncmlyrics).saveAs(path)
console.print(f"--> {str(path)}")
api.saveCookies()

View File

@ -1,7 +1,7 @@
from dataclasses import dataclass
from http.cookiejar import LoadError, MozillaCookieJar
from json import dumps as dumpJson, JSONDecodeError
from typing import Any, Iterable, Self
from json import dumps as dumpJson
from typing import Iterable
from httpx import Client as HttpXClient
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 .error import (
NCMApiResponseParseError,
NCMApiRequestError,
NCMApiRetryLimitExceededError,
UnsupportedPureMusicTrackError,
)
from .lrc import Lrc, LrcType
from .object import NCMAlbum, NCMLyrics, NCMPlaylist, NCMTrack
REQUEST_HEADERS = {
"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:
def __init__(self, http2: bool = True) -> None:
self._cookiePath = PLATFORM.user_config_path / "cookies.txt"

34
src/ncmlyrics/enum.py Normal file
View 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()

View File

@ -13,14 +13,14 @@ class NCMApiRequestError(NCMApiError, RequestError):
"""请求网易云音乐 API 时出现错误"""
class NCMApiResponseParseError(NCMApiError):
"""解析网易云音乐 API 返回的数据时出现错误"""
class NCMApiRetryLimitExceededError(NCMApiError):
"""请求网易云音乐 API 时错误次数超过重试次数上限"""
class ObjectParseError(NCMLyricsAppError):
"""解析网易云音乐 API 返回的数据时出现错误"""
class ParseLinkError(NCMLyricsAppError):
"""无法解析此分享链接"""

View File

@ -1,10 +1,16 @@
from enum import Enum
from json import JSONDecodeError
from json import loads as loadJson
from pathlib import Path
from re import Match
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 .enum import LrcMetaType, LrcType
from .error import UnsupportedPureMusicTrackError
from .object import NCMLyrics
__all__ = ["LrcType", "LrcMetaType", "Lrc"]
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*$")
@ -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})?)\]")
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:
def __init__(self) -> None:
# metaType: lrcType: metaContent
self.metadata: dict[LrcMetaType, dict[LrcType, str]] = {}
# timestamp: lrcType: lrcContent
self.lyrics: dict[int, dict[LrcType, str]] = {}
# timestamp: lrcType/String: lrcContent
self.lyrics: dict[int, dict[LrcType | str, str]] = {}
def serializeLyricString(self, lrcType: LrcType, lrcStr: str) -> None:
for line in lrcStr.splitlines():
# Skip commit lines
if LRC_RE_COMMIT.match(line) is not None:
continue
@classmethod
def fromNCMLyrics(cls, lyrics: NCMLyrics) -> Self:
if lyrics.isPureMusic:
raise UnsupportedPureMusicTrackError
# Skip NCM spical metadata lines
if LRC_RE_META_NCM_SPICAL.match(line) is not None:
continue
result = cls()
matchedMetaDataRow = LRC_RE_META.match(line)
if matchedMetaDataRow is not None:
self.appendMatchedMetaDataRow(lrcType, matchedMetaDataRow)
continue
for lrcType in LrcType:
lrcStr = lyrics.get(lrcType)
if lrcStr:
result.serializeLyricFile(lrcType, lrcStr)
matchedLyricRow = LRC_RE_LYRIC.match(line)
if matchedLyricRow is not None:
self.appendMatchedLyricRow(lrcType, matchedLyricRow)
continue
return result
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:
if timestamp in self.lyrics:
self.lyrics[timestamp][lrcType] = lyric

181
src/ncmlyrics/object.py Normal file
View 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)

View File

@ -1,5 +1,4 @@
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from re import compile as reCompile
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 .api import NCMTrack
from .error import ParseLinkError, UnsupportLinkError
from .enum import LinkType
from .error import ParseLinkError, UnsupportedLinkError
from .object import NCMTrack
RE_ANDROID_ALBUM_SHARE_LINK_PATH = reCompile(r"^/album/(?P<id>\d*)/?$")
RE_SAFE_FILENAME = reCompile(r"\*{2,}")
TRANSLATER_SAFE_FILENAME = str.maketrans({i: 0x2A for i in ("<", ">", ":", '"', "/", "\\", "|", "?")})
class LinkType(Enum):
Song = auto()
Album = auto()
Playlist = auto()
@dataclass
class Link:
type: LinkType