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
This commit is contained in:
parent
db90c5b693
commit
5492aee0d6
@ -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())
|
||||
|
@ -13,10 +13,14 @@ class NCMApiRequestError(NCMApiError, RequestError):
|
||||
"""请求网易云音乐 API 时出现错误"""
|
||||
|
||||
|
||||
class NCMApiParseError(NCMApiError):
|
||||
class NCMApiResponseParseError(NCMApiError):
|
||||
"""解析网易云音乐 API 返回的数据时出现错误"""
|
||||
|
||||
|
||||
class NCMApiRetryLimitExceededError(NCMApiError):
|
||||
"""请求网易云音乐 API 时错误次数超过重试次数上限"""
|
||||
|
||||
|
||||
class ParseLinkError(NCMLyricsAppError):
|
||||
"""无法解析此分享链接"""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user