-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
216 lines (174 loc) · 5.79 KB
/
models.py
File metadata and controls
216 lines (174 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
from datetime import datetime, timedelta
from enum import StrEnum
from functools import cache, cached_property
from typing import Annotated, Any, Literal
import streamlit as st
from pydantic import BaseModel as _BaseModel
from pydantic import (BeforeValidator, ConfigDict, Field, ValidationError,
field_validator, model_validator)
from pydantic.alias_generators import to_camel
from pyncm.apis.album import GetAlbumComments
from pyncm.apis.track import GetTrackAudio, GetTrackLyrics
Timestamp = Annotated[datetime, BeforeValidator(
lambda val: datetime.fromtimestamp(val / 1000)
if isinstance(val, int) else val)]
class BaseModel(_BaseModel):
model_config = ConfigDict(
alias_generator=to_camel,
validate_by_name=True,
validate_by_alias=True,
)
class BaseEntity(BaseModel):
id: int
name: str
translations: list[str] = Field(alias="tns", default_factory=list)
alias: list[str] = Field(alias="alia", default_factory=list)
def __hash__(self) -> int:
return hash(self.id)
class Artist(BaseEntity):
pass
class BaseAlbum(BaseEntity):
pic_url: str
class AudioQuality(StrEnum):
STANDARD = "standard"
HIGHER = "higher"
EXHIGH = "exhigh"
LOSSLESS = "lossless"
class AudioInfo(BaseModel):
size: int
bit_rate: int = Field(alias="br")
file_id: int = Field(alias="fid")
volume_delta: float = Field(alias="vd")
sample_rate: int = Field(alias="sr")
class AudioDetail(BaseModel):
id: int
url: str
type: str
level: AudioQuality | None = None
bit_rate: int = Field(alias="br")
simple_rate: int = Field(alias="sr")
size: int
md5: str
gain: float | None = None
peak: float | None = None
closed_gain: float | None = None
closed_peak: float | None = None
class LyricData(BaseModel):
version: int = 0
text: str = Field(alias="lyric", default="")
class TrackLyrics(BaseModel):
original: LyricData = Field(alias="lrc")
translated: LyricData = Field(alias="tlyric", default_factory=LyricData)
romanized: LyricData = Field(alias="romalrc", default_factory=LyricData)
class BaseTrack(BaseEntity):
artists: list[Artist] = Field(alias="ar")
duration: timedelta = Field(alias="dt")
track_number: int = Field(alias="no")
music_video_id: int = Field(alias="mv")
radio_program_id: int = Field(alias="djId")
popularity: int = Field(alias="pop")
qualities: dict[AudioQuality, AudioInfo]
@field_validator("duration", mode="before")
@classmethod
def parse_duration(cls, v: Any) -> timedelta:
return timedelta(milliseconds=v) if isinstance(v, int) else v
@model_validator(mode="before")
@classmethod
def build_qualities(cls, data: Any) -> Any:
if isinstance(data, dict):
quality_mapping = {
"l": AudioQuality.STANDARD, # 128000
"m": AudioQuality.HIGHER, # 192000
"h": AudioQuality.EXHIGH, # 320000
"sq": AudioQuality.LOSSLESS # 321000+
}
data["qualities"] = {
quality_mapping[field]: data[field]
for field in quality_mapping
if data.get(field) is not None
and isinstance(data[field], dict)
}
return data
@cached_property
def highest_quality(self) -> AudioQuality:
return max(
self.qualities.keys(),
key=lambda k: self.qualities[k].bit_rate
)
@cache
def detail(self, quality: AudioQuality) -> AudioDetail | None:
if (info := self.qualities.get(quality)) is None:
info = self.qualities[self.highest_quality]
response = GetTrackAudio([self.id], bitrate=info.bit_rate)
try:
return AudioDetail(**response["data"][0]) # type: ignore
except KeyError:
st.error(f"Failed to get audio detail for track {self.id}")
st.json(response)
except ValidationError:
return None
@cached_property
def lyrics(self) -> TrackLyrics:
response = GetTrackLyrics(str(self.id))
return TrackLyrics(**response) # type: ignore
class Track(BaseTrack):
album: BaseAlbum = Field(alias="al")
is_single: bool = Field(alias="single")
publish_time: Timestamp
class AlbumInfo(BaseAlbum):
blur_pic_url: str
artist: Artist
artists: list[Artist]
company: str
company_id: int = 0
brief_desc: str = ""
description: str | None = None
type: str
sub_type: str
size: int
tag: None = None
award_tags: None
display_tags: None
publish_time: Timestamp
class Album(BaseModel):
songs: list[BaseTrack]
info: AlbumInfo = Field(alias="album")
@cache
def comments(self, page=0) -> "AlbumComments":
CHUNK_SIZE = 20
response = GetAlbumComments(
album_id=str(self.info.id),
offset=page * CHUNK_SIZE,
limit=CHUNK_SIZE
)
try:
return AlbumComments(**response) # type: ignore
except ValidationError:
st.error(f"Failed to get comments for album {self.info.id}")
st.json(response)
st.stop()
def __hash__(self) -> int:
return hash(self.info.id)
class User(BaseModel):
user_id: int
nickname: str
avatar_url: str
class Comment(BaseModel):
comment_id: int
user: User
content: str
rich_content: str
time: Timestamp
time_str: str
liked_count: int
parent_comment_id: int = 0
class AlbumComments(BaseModel):
is_musician: bool
cnum: Literal[0]
top_comments: list[Comment] = Field(max_length=0)
hot_comments: list[Comment] = []
more_hot: bool = False
comment_banner: None = None
comments: list[Comment]
total_count: int | None = None
more: bool = False