From 3c8a46d039a597a47d94ec4a33af104a005a00b6 Mon Sep 17 00:00:00 2001 From: Puqns67 Date: Tue, 15 Oct 2024 20:18:48 +0800 Subject: [PATCH] chore: move enums and objects to single file --- src/ncmlyrics/__main__.py | 12 +-- src/ncmlyrics/api.py | 173 +----------------------------------- src/ncmlyrics/enum.py | 34 +++++++ src/ncmlyrics/error.py | 8 +- src/ncmlyrics/lrc.py | 93 ++++++++++---------- src/ncmlyrics/object.py | 181 ++++++++++++++++++++++++++++++++++++++ src/ncmlyrics/util.py | 12 +-- 7 files changed, 279 insertions(+), 234 deletions(-) create mode 100644 src/ncmlyrics/enum.py create mode 100644 src/ncmlyrics/object.py diff --git a/src/ncmlyrics/__main__.py b/src/ncmlyrics/__main__.py index 5d0c731..fed7811 100644 --- a/src/ncmlyrics/__main__.py +++ b/src/ncmlyrics/__main__.py @@ -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() diff --git a/src/ncmlyrics/api.py b/src/ncmlyrics/api.py index 685db41..17b50d3 100644 --- a/src/ncmlyrics/api.py +++ b/src/ncmlyrics/api.py @@ -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" diff --git a/src/ncmlyrics/enum.py b/src/ncmlyrics/enum.py new file mode 100644 index 0000000..6966d9e --- /dev/null +++ b/src/ncmlyrics/enum.py @@ -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() diff --git a/src/ncmlyrics/error.py b/src/ncmlyrics/error.py index d8e2f6b..eeadeaa 100644 --- a/src/ncmlyrics/error.py +++ b/src/ncmlyrics/error.py @@ -13,14 +13,14 @@ class NCMApiRequestError(NCMApiError, RequestError): """请求网易云音乐 API 时出现错误""" -class NCMApiResponseParseError(NCMApiError): - """解析网易云音乐 API 返回的数据时出现错误""" - - class NCMApiRetryLimitExceededError(NCMApiError): """请求网易云音乐 API 时错误次数超过重试次数上限""" +class ObjectParseError(NCMLyricsAppError): + """解析网易云音乐 API 返回的数据时出现错误""" + + class ParseLinkError(NCMLyricsAppError): """无法解析此分享链接""" diff --git a/src/ncmlyrics/lrc.py b/src/ncmlyrics/lrc.py index 28aea81..24be0c7 100644 --- a/src/ncmlyrics/lrc.py +++ b/src/ncmlyrics/lrc.py @@ -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*\[(?Pti|ar|al|au|length|by|offset):\s*(?P.+?)\s*\]\s*$") @@ -13,60 +19,55 @@ LRC_RE_LYRIC = reCompile(r"^\s*(?P(?:\s*\[\d{1,2}:\d{1,2}(?:\.\d{1,3 LRC_RE_LYRIC_TIMELABEL = reCompile(r"\[(?P\d{1,2}):(?P\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 diff --git a/src/ncmlyrics/object.py b/src/ncmlyrics/object.py new file mode 100644 index 0000000..796ee24 --- /dev/null +++ b/src/ncmlyrics/object.py @@ -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) diff --git a/src/ncmlyrics/util.py b/src/ncmlyrics/util.py index a41e972..08a651a 100644 --- a/src/ncmlyrics/util.py +++ b/src/ncmlyrics/util.py @@ -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\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