From 5492aee0d603a6de49d59e8800e30be45a66bfa9 Mon Sep 17 00:00:00 2001 From: Puqns67 Date: Sun, 13 Oct 2024 18:11:48 +0800 Subject: [PATCH] feature: new fetch api for retry * NCMLyrics also can stores track ID, but need set value manually * Convert NCM*.fromApi() to @classmethod, and pass response directly --- src/ncmlyrics/api.py | 156 +++++++++++++++++++++++++---------------- src/ncmlyrics/error.py | 6 +- 2 files changed, 99 insertions(+), 63 deletions(-) diff --git a/src/ncmlyrics/api.py b/src/ncmlyrics/api.py index 00791a9..2485f30 100644 --- a/src/ncmlyrics/api.py +++ b/src/ncmlyrics/api.py @@ -1,12 +1,19 @@ from dataclasses import dataclass from http.cookiejar import LoadError, MozillaCookieJar -from json import dumps as dumpJson -from typing import Any, Self +from json import dumps as dumpJson, JSONDecodeError +from typing import Any, Iterable, Self -from httpx import Client as HttpClient +from httpx import Client as HttpXClient +from httpx import Request as HttpXRequest +from httpx import Response as HttpXResponse from .constant import CONFIG_API_DETAIL_TRACK_PER_REQUEST, NCM_API_BASE_URL, PLATFORM -from .error import NCMApiParseError, NCMApiRequestError, UnsupportedPureMusicTrackError +from .error import ( + NCMApiResponseParseError, + NCMApiRequestError, + NCMApiRetryLimitExceededError, + UnsupportedPureMusicTrackError, +) from .lrc import Lrc, LrcType REQUEST_HEADERS = { @@ -23,30 +30,37 @@ class NCMTrack: name: str artists: list[str] - def fromApi(data: dict) -> list[Self]: + @classmethod + def fromApi(cls, response: HttpXResponse) -> list[Self]: + try: + data: dict = response.json() + except JSONDecodeError: + raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应") + if data.get("code") != 200: - raise NCMApiParseError(f"响应码不为 200: {data["code"]}") + raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}") data = data.get("songs") if data is None: - raise NCMApiParseError("不存在 Track 对应的结构") + raise NCMApiResponseParseError("不存在单曲对应的结构") - result: list[NCMTrack] = [] + result = [] for track in data: - result.append(NCMTrack.fromData(track)) + result.append(cls.fromData(track)) return result - def fromData(data: dict) -> Self: + @classmethod + def fromData(cls, data: dict) -> Self: try: - return NCMTrack( + return cls( id=data["id"], name=data["name"], artists=[artist["name"] for artist in data["ar"]], ) except KeyError as e: - raise NCMApiParseError(f"需要的键不存在: {e}") + raise NCMApiResponseParseError(f"需要的键不存在: {e}") def link(self) -> str: return f"https://music.163.com/song?id={self.id}" @@ -58,22 +72,28 @@ class NCMAlbum: name: str tracks: list[NCMTrack] - def fromApi(data: dict) -> Self: + @classmethod + def fromApi(cls, response: HttpXResponse) -> Self: + try: + data: dict = response.json() + except JSONDecodeError: + raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应") + if data.get("code") != 200: - raise NCMApiParseError(f"响应码不为 200: {data["code"]}") + raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}") album = data.get("album") if album is None: - raise NCMApiParseError("不存在 Album 对应的结构") + raise NCMApiResponseParseError("不存在专辑对应的结构") try: - return NCMAlbum( + return cls( id=album["id"], name=album["name"], tracks=[NCMTrack.fromData(track) for track in data["songs"]], ) except KeyError as e: - raise NCMApiParseError(f"需要的键不存在: {e}") + raise NCMApiResponseParseError(f"需要的键不存在: {e}") def link(self) -> str: return f"https://music.163.com/album?id={self.id}" @@ -86,31 +106,37 @@ class NCMPlaylist: tracks: list[NCMTrack] trackIds: list[int] - def fromApi(data: dict) -> Self: + @classmethod + def fromApi(cls, response: HttpXResponse) -> Self: + try: + data: dict = response.json() + except JSONDecodeError: + raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应") + if data.get("code") != 200: - raise NCMApiParseError(f"响应码不为 200: {data["code"]}") + raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}") playlist = data.get("playlist") if playlist is None: - raise NCMApiParseError("不存在 Playlist 对应的结构") + raise NCMApiResponseParseError("不存在歌单对应的结构") try: tracks: list[NCMTrack] = [] trackIds: list[int] = [track["id"] for track in playlist["trackIds"]] for track in playlist["tracks"]: - parsedTrack: NCMTrack = NCMTrack.fromData(track) + parsedTrack = NCMTrack.fromData(track) trackIds.remove(parsedTrack.id) tracks.append(parsedTrack) - return NCMPlaylist( + return cls( id=playlist["id"], name=playlist["name"], tracks=tracks, trackIds=trackIds, ) except KeyError as e: - raise NCMApiParseError(f"需要的键不存在: {e}") + raise NCMApiResponseParseError(f"需要的键不存在: {e}") def link(self) -> str: return f"https://music.163.com/playlist?id={self.id}" @@ -122,17 +148,28 @@ class NCMPlaylist: @dataclass class NCMLyrics: + id: int | None isPureMusic: bool data: Any | None - def fromApi(data: dict) -> Self: + @classmethod + def fromApi(cls, response: HttpXResponse) -> Self: + try: + data: dict = response.json() + except JSONDecodeError: + raise NCMApiResponseParseError("无法以预期的 Json 格式解析响应") + if data.get("code") != 200: - raise NCMApiParseError(f"响应码不为 200: {data["code"]}") + raise NCMApiResponseParseError(f"响应码不为 200: {data["code"]}") if data.get("pureMusic") is True: - return NCMLyrics(isPureMusic=True, data=None) + return cls(id=None, isPureMusic=True, data=None) - return NCMLyrics(isPureMusic=False, data=data) + 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: @@ -153,9 +190,6 @@ class NCMLyrics: class NCMApi: - _cookieJar: MozillaCookieJar - _httpClient: HttpClient - def __init__(self, http2: bool = True) -> None: self._cookieJar = MozillaCookieJar() @@ -164,25 +198,38 @@ class NCMApi: except FileNotFoundError | LoadError: pass - self._httpClient = HttpClient( + self._httpClient = HttpXClient( base_url=NCM_API_BASE_URL, cookies=self._cookieJar, headers=REQUEST_HEADERS, http2=http2, ) + def _fetch(self, request: HttpXRequest, retry: int | None = 4) -> HttpXResponse: + if retry: # None => Disable retry + if retry < 0: + retry = 0 + + while retry < 0: + try: + return self._httpClient.send(request) + except Exception: + retry -= 1 + + raise NCMApiRetryLimitExceededError + + else: + try: + return self._httpClient.send(request) + except Exception: + raise NCMApiRequestError + def saveCookies(self) -> None: self._cookieJar.save(str(PLATFORM.user_config_path / "cookies.txt")) def getDetailsForTrack(self, trackId: int) -> NCMTrack: - params = {"c": f"[{{'id':{trackId}}}]"} - - try: - response = self._httpClient.request("GET", "/v3/song/detail", params=params) - except BaseException: - raise NCMApiRequestError - - return NCMTrack.fromApi(response.json()).pop() + request = self._httpClient.build_request("GET", "/v3/song/detail", params={"c": f"[{{'id':{trackId}}}]"}) + return NCMTrack.fromApi(self._fetch(request)).pop() def getDetailsForTracks(self, trackIds: list[int]) -> list[NCMTrack]: result: list[NCMTrack] = [] @@ -201,34 +248,21 @@ class NCMApi: ) } - try: - response = self._httpClient.request("GET", "/v3/song/detail", params=params) - except BaseException: - raise NCMApiRequestError + request = self._httpClient.build_request("GET", "/v3/song/detail", params=params) - result.extend(NCMTrack.fromApi(response.json())) + result.extend(NCMTrack.fromApi(self._fetch(request))) seek += CONFIG_API_DETAIL_TRACK_PER_REQUEST return result def getDetailsForAlbum(self, albumId: int) -> NCMAlbum: - try: - response = self._httpClient.request("GET", f"/v1/album/{albumId}") - except BaseException: - raise NCMApiRequestError - - return NCMAlbum.fromApi(response.json()) + request = self._httpClient.build_request("GET", f"/v1/album/{albumId}") + return NCMAlbum.fromApi(self._fetch(request)) def getDetailsForPlaylist(self, playlistId: int) -> NCMPlaylist: - params = {"id": playlistId} - - try: - response = self._httpClient.request("GET", "/v6/playlist/detail", params=params) - except BaseException: - raise NCMApiRequestError - - return NCMPlaylist.fromApi(response.json()) + request = self._httpClient.build_request("GET", "/v6/playlist/detail", params={"id": playlistId}) + return NCMPlaylist.fromApi(self._fetch(request)) def getLyricsByTrack(self, trackId: int) -> NCMLyrics: params = { @@ -243,9 +277,7 @@ class NCMApi: "yrv": 0, } - try: - response = self._httpClient.request("GET", "/song/lyric/v1", params=params) - except BaseException: - raise NCMApiRequestError + request = self._httpClient.build_request("GET", "/song/lyric/v1", params=params) + return NCMLyrics.fromApi(self._fetch(request)).withId(trackId) return NCMLyrics.fromApi(response.json()) diff --git a/src/ncmlyrics/error.py b/src/ncmlyrics/error.py index 4925bf5..d8e2f6b 100644 --- a/src/ncmlyrics/error.py +++ b/src/ncmlyrics/error.py @@ -13,10 +13,14 @@ class NCMApiRequestError(NCMApiError, RequestError): """请求网易云音乐 API 时出现错误""" -class NCMApiParseError(NCMApiError): +class NCMApiResponseParseError(NCMApiError): """解析网易云音乐 API 返回的数据时出现错误""" +class NCMApiRetryLimitExceededError(NCMApiError): + """请求网易云音乐 API 时错误次数超过重试次数上限""" + + class ParseLinkError(NCMLyricsAppError): """无法解析此分享链接"""