]>
Commit | Line | Data |
---|---|---|
e3a3ed8a | 1 | import hashlib |
46d09f87 | 2 | import itertools |
db4678e4 | 3 | import json |
e3a3ed8a | 4 | import random |
ac668111 | 5 | import re |
db4678e4 | 6 | import time |
af1fa623 | 7 | |
8 | from .common import InfoExtractor | |
db4678e4 | 9 | from ..aes import aes_ecb_encrypt, pkcs7_padding |
db4678e4 | 10 | from ..utils import ( |
11 | ExtractorError, | |
db4678e4 | 12 | int_or_none, |
f980df73 | 13 | join_nonempty, |
14 | str_or_none, | |
15 | strftime_or_none, | |
16 | traverse_obj, | |
17 | unified_strdate, | |
18 | url_or_none, | |
19 | urljoin, | |
20 | variadic, | |
db4678e4 | 21 | ) |
af1fa623 | 22 | |
23 | ||
24 | class NetEaseMusicBaseIE(InfoExtractor): | |
7a03f88c | 25 | # XXX: _extract_formats logic depends on the order of the levels in each tier |
26 | _LEVELS = ( | |
27 | 'standard', # free tier; 标准; 128kbps mp3 or aac | |
28 | 'higher', # free tier; 192kbps mp3 or aac | |
29 | 'exhigh', # free tier; 极高 (HQ); 320kbps mp3 or aac | |
30 | 'lossless', # VIP tier; 无损 (SQ); 48kHz/16bit flac | |
31 | 'hires', # VIP tier; 高解析度无损 (Hi-Res); 192kHz/24bit flac | |
32 | 'jyeffect', # VIP tier; 高清臻音 (Spatial Audio); 96kHz/24bit flac | |
33 | 'jymaster', # SVIP tier; 超清母带 (Master); 192kHz/24bit flac | |
34 | 'sky', # SVIP tier; 沉浸环绕声 (Surround Audio); flac | |
35 | ) | |
af1fa623 | 36 | _API_BASE = 'http://music.163.com/api/' |
f980df73 | 37 | _GEO_BYPASS = False |
af1fa623 | 38 | |
f980df73 | 39 | @staticmethod |
7a03f88c | 40 | def _kilo_or_none(value): |
f980df73 | 41 | return int_or_none(value, scale=1000) |
42 | ||
43 | def _create_eapi_cipher(self, api_path, query_body, cookies): | |
44 | request_text = json.dumps({**query_body, 'header': cookies}, separators=(',', ':')) | |
45 | ||
46 | message = f'nobody{api_path}use{request_text}md5forencrypt'.encode('latin1') | |
e3a3ed8a | 47 | msg_digest = hashlib.md5(message).hexdigest() |
f980df73 | 48 | |
49 | data = pkcs7_padding(list(str.encode( | |
50 | f'{api_path}-36cd479b6b5-{request_text}-36cd479b6b5-{msg_digest}'))) | |
51 | encrypted = bytes(aes_ecb_encrypt(data, list(b'e82ckenh8dichen8'))) | |
52 | return f'params={encrypted.hex().upper()}'.encode() | |
53 | ||
54 | def _download_eapi_json(self, path, video_id, query_body, headers={}, **kwargs): | |
55 | cookies = { | |
56 | 'osver': 'undefined', | |
57 | 'deviceId': 'undefined', | |
db4678e4 | 58 | 'appver': '8.0.0', |
59 | 'versioncode': '140', | |
f980df73 | 60 | 'mobilename': 'undefined', |
db4678e4 | 61 | 'buildver': '1623435496', |
62 | 'resolution': '1920x1080', | |
63 | '__csrf': '', | |
64 | 'os': 'pc', | |
f980df73 | 65 | 'channel': 'undefined', |
e3a3ed8a | 66 | 'requestId': f'{int(time.time() * 1000)}_{random.randint(0, 1000):04}', |
f980df73 | 67 | **traverse_obj(self._get_cookies(self._API_BASE), { |
68 | 'MUSIC_U': ('MUSIC_U', {lambda i: i.value}), | |
add96eb9 | 69 | }), |
db4678e4 | 70 | } |
f980df73 | 71 | return self._download_json( |
72 | urljoin('https://interface3.music.163.com/', f'/eapi{path}'), video_id, | |
73 | data=self._create_eapi_cipher(f'/api{path}', query_body, cookies), headers={ | |
74 | 'Referer': 'https://music.163.com', | |
75 | 'Cookie': '; '.join([f'{k}={v}' for k, v in cookies.items()]), | |
76 | **headers, | |
77 | }, **kwargs) | |
db4678e4 | 78 | |
7a03f88c | 79 | def _call_player_api(self, song_id, level): |
f980df73 | 80 | return self._download_eapi_json( |
7a03f88c | 81 | '/song/enhance/player/url/v1', song_id, |
82 | {'ids': f'[{song_id}]', 'level': level, 'encodeType': 'flac'}, | |
83 | note=f'Downloading song URL info: level {level}') | |
db4678e4 | 84 | |
7a03f88c | 85 | def _extract_formats(self, info): |
af1fa623 | 86 | formats = [] |
db4678e4 | 87 | song_id = info['id'] |
7a03f88c | 88 | for level in self._LEVELS: |
89 | song = traverse_obj( | |
90 | self._call_player_api(song_id, level), ('data', lambda _, v: url_or_none(v['url']), any)) | |
91 | if not song: | |
92 | break # Media is not available due to removal or geo-restriction | |
93 | actual_level = song.get('level') | |
94 | if actual_level and actual_level != level: | |
95 | if level in ('lossless', 'jymaster'): | |
96 | break # We've already extracted the highest level of the user's account tier | |
af1fa623 | 97 | continue |
7a03f88c | 98 | formats.append({ |
99 | 'url': song['url'], | |
100 | 'format_id': level, | |
101 | 'vcodec': 'none', | |
102 | **traverse_obj(song, { | |
103 | 'ext': ('type', {str}), | |
104 | 'abr': ('br', {self._kilo_or_none}), | |
105 | 'filesize': ('size', {int_or_none}), | |
106 | }), | |
107 | }) | |
108 | if not actual_level: | |
109 | break # Only 1 level is available if API does not return a value (netease:program) | |
db4678e4 | 110 | if not formats: |
7a03f88c | 111 | self.raise_geo_restricted( |
112 | 'No media links found; possibly due to geo restriction', countries=['CN']) | |
af1fa623 | 113 | return formats |
114 | ||
7a03f88c | 115 | def _query_api(self, endpoint, video_id, note): |
f980df73 | 116 | result = self._download_json( |
117 | f'{self._API_BASE}{endpoint}', video_id, note, headers={'Referer': self._API_BASE}) | |
118 | code = traverse_obj(result, ('code', {int})) | |
119 | message = traverse_obj(result, ('message', {str})) or '' | |
120 | if code == -462: | |
121 | self.raise_login_required(f'Login required to download: {message}') | |
122 | elif code != 200: | |
123 | raise ExtractorError(f'Failed to get meta info: {code} {message}') | |
124 | return result | |
125 | ||
126 | def _get_entries(self, songs_data, entry_keys=None, id_key='id', name_key='name'): | |
127 | for song in traverse_obj(songs_data, ( | |
128 | *variadic(entry_keys, (str, bytes, dict, set)), | |
129 | lambda _, v: int_or_none(v[id_key]) is not None)): | |
130 | song_id = str(song[id_key]) | |
131 | yield self.url_result( | |
132 | f'http://music.163.com/#/song?id={song_id}', NetEaseMusicIE, | |
133 | song_id, traverse_obj(song, (name_key, {str}))) | |
af1fa623 | 134 | |
135 | ||
136 | class NetEaseMusicIE(NetEaseMusicBaseIE): | |
137 | IE_NAME = 'netease:song' | |
6ce89aec | 138 | IE_DESC = '网易云音乐' |
7a03f88c | 139 | _VALID_URL = r'https?://(?:y\.)?music\.163\.com/(?:[#m]/)?song\?.*?\bid=(?P<id>[0-9]+)' |
af1fa623 | 140 | _TESTS = [{ |
7a03f88c | 141 | 'url': 'https://music.163.com/#/song?id=550136151', |
af1fa623 | 142 | 'info_dict': { |
7a03f88c | 143 | 'id': '550136151', |
af1fa623 | 144 | 'ext': 'mp3', |
7a03f88c | 145 | 'title': 'It\'s Ok (Live)', |
146 | 'creators': 'count:10', | |
f980df73 | 147 | 'timestamp': 1522944000, |
148 | 'upload_date': '20180405', | |
7a03f88c | 149 | 'description': 'md5:9fd07059c2ccee3950dc8363429a3135', |
150 | 'duration': 197, | |
f980df73 | 151 | 'thumbnail': r're:^http.*\.jpg', |
46acc418 L |
152 | 'album': '偶像练习生 表演曲目合集', |
153 | 'average_rating': int, | |
7a03f88c | 154 | 'album_artists': ['偶像练习生'], |
af1fa623 | 155 | }, |
af1fa623 | 156 | }, { |
af1fa623 | 157 | 'url': 'http://music.163.com/song?id=17241424', |
158 | 'info_dict': { | |
159 | 'id': '17241424', | |
160 | 'ext': 'mp3', | |
161 | 'title': 'Opus 28', | |
af1fa623 | 162 | 'upload_date': '20080211', |
163 | 'timestamp': 1202745600, | |
f980df73 | 164 | 'duration': 263, |
165 | 'thumbnail': r're:^http.*\.jpg', | |
46acc418 L |
166 | 'album': 'Piano Solos Vol. 2', |
167 | 'album_artist': 'Dustin O\'Halloran', | |
168 | 'average_rating': int, | |
7a03f88c | 169 | 'description': '[00:05.00]纯音乐,请欣赏\n', |
170 | 'album_artists': ['Dustin O\'Halloran'], | |
171 | 'creators': ['Dustin O\'Halloran'], | |
172 | 'subtitles': {'lyrics': [{'ext': 'lrc'}]}, | |
75af5d59 | 173 | }, |
db4678e4 | 174 | }, { |
175 | 'url': 'https://y.music.163.com/m/song?app_version=8.8.45&id=95670&uct2=sKnvS4+0YStsWkqsPhFijw%3D%3D&dlt=0846', | |
7a03f88c | 176 | 'md5': 'b896be78d8d34bd7bb665b26710913ff', |
db4678e4 | 177 | 'info_dict': { |
178 | 'id': '95670', | |
179 | 'ext': 'mp3', | |
180 | 'title': '国际歌', | |
db4678e4 | 181 | 'upload_date': '19911130', |
182 | 'timestamp': 691516800, | |
183 | 'description': 'md5:1ba2f911a2b0aa398479f595224f2141', | |
f980df73 | 184 | 'subtitles': {'lyrics': [{'ext': 'lrc'}]}, |
185 | 'duration': 268, | |
186 | 'alt_title': '伴唱:现代人乐队 合唱:总政歌舞团', | |
187 | 'thumbnail': r're:^http.*\.jpg', | |
46acc418 L |
188 | 'average_rating': int, |
189 | 'album': '红色摇滚', | |
190 | 'album_artist': '侯牧人', | |
7a03f88c | 191 | 'creators': ['马备'], |
192 | 'album_artists': ['侯牧人'], | |
f980df73 | 193 | }, |
194 | }, { | |
195 | 'url': 'http://music.163.com/#/song?id=32102397', | |
196 | 'md5': '3e909614ce09b1ccef4a3eb205441190', | |
197 | 'info_dict': { | |
198 | 'id': '32102397', | |
199 | 'ext': 'mp3', | |
200 | 'title': 'Bad Blood', | |
7a03f88c | 201 | 'creators': ['Taylor Swift', 'Kendrick Lamar'], |
f980df73 | 202 | 'upload_date': '20150516', |
203 | 'timestamp': 1431792000, | |
204 | 'description': 'md5:21535156efb73d6d1c355f95616e285a', | |
205 | 'subtitles': {'lyrics': [{'ext': 'lrc'}]}, | |
206 | 'duration': 199, | |
207 | 'thumbnail': r're:^http.*\.jpg', | |
46acc418 L |
208 | 'album': 'Bad Blood', |
209 | 'average_rating': int, | |
210 | 'album_artist': 'Taylor Swift', | |
db4678e4 | 211 | }, |
f980df73 | 212 | 'skip': 'Blocked outside Mainland China', |
213 | }, { | |
214 | 'note': 'Has translated name.', | |
215 | 'url': 'http://music.163.com/#/song?id=22735043', | |
216 | 'info_dict': { | |
217 | 'id': '22735043', | |
218 | 'ext': 'mp3', | |
219 | 'title': '소원을 말해봐 (Genie)', | |
7a03f88c | 220 | 'creators': ['少女时代'], |
f980df73 | 221 | 'upload_date': '20100127', |
222 | 'timestamp': 1264608000, | |
223 | 'description': 'md5:03d1ffebec3139aa4bafe302369269c5', | |
224 | 'subtitles': {'lyrics': [{'ext': 'lrc'}]}, | |
225 | 'duration': 229, | |
226 | 'alt_title': '说出愿望吧(Genie)', | |
227 | 'thumbnail': r're:^http.*\.jpg', | |
46acc418 L |
228 | 'average_rating': int, |
229 | 'album': 'Oh!', | |
230 | 'album_artist': '少女时代', | |
f980df73 | 231 | }, |
232 | 'skip': 'Blocked outside Mainland China', | |
af1fa623 | 233 | }] |
234 | ||
235 | def _process_lyrics(self, lyrics_info): | |
f980df73 | 236 | original = traverse_obj(lyrics_info, ('lrc', 'lyric', {str})) |
237 | translated = traverse_obj(lyrics_info, ('tlyric', 'lyric', {str})) | |
238 | ||
239 | if not original or original == '[99:00.00]纯音乐,请欣赏\n': | |
240 | return None | |
af1fa623 | 241 | |
242 | if not translated: | |
f980df73 | 243 | return { |
244 | 'lyrics': [{'data': original, 'ext': 'lrc'}], | |
245 | } | |
af1fa623 | 246 | |
247 | lyrics_expr = r'(\[[0-9]{2}:[0-9]{2}\.[0-9]{2,}\])([^\n]+)' | |
248 | original_ts_texts = re.findall(lyrics_expr, original) | |
f980df73 | 249 | translation_ts_dict = dict(re.findall(lyrics_expr, translated)) |
250 | ||
251 | merged = '\n'.join( | |
252 | join_nonempty(f'{timestamp}{text}', translation_ts_dict.get(timestamp, ''), delim=' / ') | |
253 | for timestamp, text in original_ts_texts) | |
254 | ||
255 | return { | |
256 | 'lyrics_merged': [{'data': merged, 'ext': 'lrc'}], | |
257 | 'lyrics': [{'data': original, 'ext': 'lrc'}], | |
258 | 'lyrics_translated': [{'data': translated, 'ext': 'lrc'}], | |
259 | } | |
af1fa623 | 260 | |
261 | def _real_extract(self, url): | |
262 | song_id = self._match_id(url) | |
263 | ||
7a03f88c | 264 | info = self._query_api( |
f980df73 | 265 | f'song/detail?id={song_id}&ids=%5B{song_id}%5D', song_id, 'Downloading song info')['songs'][0] |
af1fa623 | 266 | |
7a03f88c | 267 | formats = self._extract_formats(info) |
af1fa623 | 268 | |
7a03f88c | 269 | lyrics = self._process_lyrics(self._query_api( |
f980df73 | 270 | f'song/lyric?id={song_id}&lv=-1&tv=-1', song_id, 'Downloading lyrics data')) |
271 | lyric_data = { | |
272 | 'description': traverse_obj(lyrics, (('lyrics_merged', 'lyrics'), 0, 'data'), get_all=False), | |
273 | 'subtitles': lyrics, | |
274 | } if lyrics else {} | |
af1fa623 | 275 | |
276 | return { | |
277 | 'id': song_id, | |
af1fa623 | 278 | 'formats': formats, |
f980df73 | 279 | 'alt_title': '/'.join(traverse_obj(info, (('transNames', 'alias'), ...))) or None, |
7a03f88c | 280 | 'creators': traverse_obj(info, ('artists', ..., 'name')) or None, |
281 | 'album_artists': traverse_obj(info, ('album', 'artists', ..., 'name')) or None, | |
f980df73 | 282 | **lyric_data, |
283 | **traverse_obj(info, { | |
284 | 'title': ('name', {str}), | |
7a03f88c | 285 | 'timestamp': ('album', 'publishTime', {self._kilo_or_none}), |
f980df73 | 286 | 'thumbnail': ('album', 'picUrl', {url_or_none}), |
7a03f88c | 287 | 'duration': ('duration', {self._kilo_or_none}), |
46acc418 L |
288 | 'album': ('album', 'name', {str}), |
289 | 'average_rating': ('score', {int_or_none}), | |
f980df73 | 290 | }), |
af1fa623 | 291 | } |
292 | ||
293 | ||
294 | class NetEaseMusicAlbumIE(NetEaseMusicBaseIE): | |
295 | IE_NAME = 'netease:album' | |
ed848087 | 296 | IE_DESC = '网易云音乐 - 专辑' |
7a03f88c | 297 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?album\?id=(?P<id>[0-9]+)' |
f980df73 | 298 | _TESTS = [{ |
299 | 'url': 'https://music.163.com/#/album?id=133153666', | |
300 | 'info_dict': { | |
301 | 'id': '133153666', | |
302 | 'title': '桃几的翻唱', | |
303 | 'upload_date': '20210913', | |
304 | 'description': '桃几2021年翻唱合集', | |
305 | 'thumbnail': r're:^http.*\.jpg', | |
306 | }, | |
7a03f88c | 307 | 'playlist_mincount': 12, |
f980df73 | 308 | }, { |
af1fa623 | 309 | 'url': 'http://music.163.com/#/album?id=220780', |
310 | 'info_dict': { | |
311 | 'id': '220780', | |
f980df73 | 312 | 'title': 'B\'Day', |
313 | 'upload_date': '20060904', | |
314 | 'description': 'md5:71a74e1d8f392d88cf1bbe48879ad0b0', | |
315 | 'thumbnail': r're:^http.*\.jpg', | |
af1fa623 | 316 | }, |
317 | 'playlist_count': 23, | |
f980df73 | 318 | }] |
af1fa623 | 319 | |
320 | def _real_extract(self, url): | |
321 | album_id = self._match_id(url) | |
f980df73 | 322 | webpage = self._download_webpage(f'https://music.163.com/album?id={album_id}', album_id) |
323 | ||
324 | songs = self._search_json( | |
325 | r'<textarea[^>]+\bid="song-list-pre-data"[^>]*>', webpage, 'metainfo', album_id, | |
326 | end_pattern=r'</textarea>', contains_pattern=r'\[(?s:.+)\]') | |
327 | metainfo = { | |
328 | 'title': self._og_search_property('title', webpage, 'title', fatal=False), | |
329 | 'description': self._html_search_regex( | |
330 | (rf'<div[^>]+\bid="album-desc-{suffix}"[^>]*>(.*?)</div>' for suffix in ('more', 'dot')), | |
331 | webpage, 'description', flags=re.S, fatal=False), | |
332 | 'thumbnail': self._og_search_property('image', webpage, 'thumbnail', fatal=False), | |
333 | 'upload_date': unified_strdate(self._html_search_meta('music:release_date', webpage, 'date', fatal=False)), | |
334 | } | |
335 | return self.playlist_result(self._get_entries(songs), album_id, **metainfo) | |
af1fa623 | 336 | |
337 | ||
338 | class NetEaseMusicSingerIE(NetEaseMusicBaseIE): | |
339 | IE_NAME = 'netease:singer' | |
ed848087 | 340 | IE_DESC = '网易云音乐 - 歌手' |
7a03f88c | 341 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?artist\?id=(?P<id>[0-9]+)' |
af1fa623 | 342 | _TESTS = [{ |
343 | 'note': 'Singer has aliases.', | |
344 | 'url': 'http://music.163.com/#/artist?id=10559', | |
345 | 'info_dict': { | |
346 | 'id': '10559', | |
f980df73 | 347 | 'title': '张惠妹 - aMEI;阿妹;阿密特', |
af1fa623 | 348 | }, |
349 | 'playlist_count': 50, | |
350 | }, { | |
351 | 'note': 'Singer has translated name.', | |
352 | 'url': 'http://music.163.com/#/artist?id=124098', | |
353 | 'info_dict': { | |
354 | 'id': '124098', | |
355 | 'title': '李昇基 - 이승기', | |
356 | }, | |
357 | 'playlist_count': 50, | |
f980df73 | 358 | }, { |
359 | 'note': 'Singer with both translated and alias', | |
360 | 'url': 'https://music.163.com/#/artist?id=159692', | |
361 | 'info_dict': { | |
362 | 'id': '159692', | |
363 | 'title': '初音ミク - 初音未来;Hatsune Miku', | |
364 | }, | |
365 | 'playlist_count': 50, | |
af1fa623 | 366 | }] |
367 | ||
368 | def _real_extract(self, url): | |
369 | singer_id = self._match_id(url) | |
370 | ||
7a03f88c | 371 | info = self._query_api( |
f980df73 | 372 | f'artist/{singer_id}?id={singer_id}', singer_id, note='Downloading singer data') |
373 | ||
374 | name = join_nonempty( | |
375 | traverse_obj(info, ('artist', 'name', {str})), | |
376 | join_nonempty(*traverse_obj(info, ('artist', ('trans', ('alias', ...)), {str})), delim=';'), | |
377 | delim=' - ') | |
378 | ||
379 | return self.playlist_result(self._get_entries(info, 'hotSongs'), singer_id, name) | |
af1fa623 | 380 | |
381 | ||
382 | class NetEaseMusicListIE(NetEaseMusicBaseIE): | |
383 | IE_NAME = 'netease:playlist' | |
ed848087 | 384 | IE_DESC = '网易云音乐 - 歌单' |
7a03f88c | 385 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?(?:playlist|discover/toplist)\?id=(?P<id>[0-9]+)' |
af1fa623 | 386 | _TESTS = [{ |
387 | 'url': 'http://music.163.com/#/playlist?id=79177352', | |
388 | 'info_dict': { | |
389 | 'id': '79177352', | |
390 | 'title': 'Billboard 2007 Top 100', | |
f980df73 | 391 | 'description': 'md5:12fd0819cab2965b9583ace0f8b7b022', |
392 | 'tags': ['欧美'], | |
393 | 'uploader': '浑然破灭', | |
394 | 'uploader_id': '67549805', | |
395 | 'timestamp': int, | |
396 | 'upload_date': r're:\d{8}', | |
af1fa623 | 397 | }, |
f980df73 | 398 | 'playlist_mincount': 95, |
399 | }, { | |
400 | 'note': 'Toplist/Charts sample', | |
401 | 'url': 'https://music.163.com/#/discover/toplist?id=60198', | |
402 | 'info_dict': { | |
403 | 'id': '60198', | |
404 | 'title': 're:美国Billboard榜 [0-9]{4}-[0-9]{2}-[0-9]{2}', | |
405 | 'description': '美国Billboard排行榜', | |
406 | 'tags': ['流行', '欧美', '榜单'], | |
407 | 'uploader': 'Billboard公告牌', | |
408 | 'uploader_id': '48171', | |
409 | 'timestamp': int, | |
410 | 'upload_date': r're:\d{8}', | |
411 | }, | |
412 | 'playlist_count': 100, | |
af1fa623 | 413 | }, { |
414 | 'note': 'Toplist/Charts sample', | |
415 | 'url': 'http://music.163.com/#/discover/toplist?id=3733003', | |
416 | 'info_dict': { | |
417 | 'id': '3733003', | |
7a03f88c | 418 | 'title': 're:韩国Melon排行榜周榜(?: [0-9]{4}-[0-9]{2}-[0-9]{2})?', |
af1fa623 | 419 | 'description': 'md5:73ec782a612711cadc7872d9c1e134fc', |
7a03f88c | 420 | 'upload_date': '20200109', |
421 | 'uploader_id': '2937386', | |
422 | 'tags': ['韩语', '榜单'], | |
423 | 'uploader': 'Melon榜单', | |
424 | 'timestamp': 1578569373, | |
af1fa623 | 425 | }, |
426 | 'playlist_count': 50, | |
427 | }] | |
428 | ||
429 | def _real_extract(self, url): | |
430 | list_id = self._match_id(url) | |
431 | ||
f980df73 | 432 | info = self._download_eapi_json( |
433 | '/v3/playlist/detail', list_id, | |
434 | {'id': list_id, 't': '-1', 'n': '500', 's': '0'}, | |
add96eb9 | 435 | note='Downloading playlist info') |
af1fa623 | 436 | |
f980df73 | 437 | metainfo = traverse_obj(info, ('playlist', { |
438 | 'title': ('name', {str}), | |
439 | 'description': ('description', {str}), | |
440 | 'tags': ('tags', ..., {str}), | |
441 | 'uploader': ('creator', 'nickname', {str}), | |
442 | 'uploader_id': ('creator', 'userId', {str_or_none}), | |
7a03f88c | 443 | 'timestamp': ('updateTime', {self._kilo_or_none}), |
f980df73 | 444 | })) |
445 | if traverse_obj(info, ('playlist', 'specialType')) == 10: | |
446 | metainfo['title'] = f'{metainfo.get("title")} {strftime_or_none(metainfo.get("timestamp"), "%Y-%m-%d")}' | |
af1fa623 | 447 | |
f980df73 | 448 | return self.playlist_result(self._get_entries(info, ('playlist', 'tracks')), list_id, **metainfo) |
af1fa623 | 449 | |
450 | ||
451 | class NetEaseMusicMvIE(NetEaseMusicBaseIE): | |
452 | IE_NAME = 'netease:mv' | |
ed848087 | 453 | IE_DESC = '网易云音乐 - MV' |
7a03f88c | 454 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?mv\?id=(?P<id>[0-9]+)' |
f980df73 | 455 | _TESTS = [{ |
456 | 'url': 'https://music.163.com/#/mv?id=10958064', | |
457 | 'info_dict': { | |
458 | 'id': '10958064', | |
459 | 'ext': 'mp4', | |
460 | 'title': '交换余生', | |
461 | 'description': 'md5:e845872cff28820642a2b02eda428fea', | |
7a03f88c | 462 | 'creators': ['林俊杰'], |
f980df73 | 463 | 'upload_date': '20200916', |
464 | 'thumbnail': r're:http.*\.jpg', | |
465 | 'duration': 364, | |
466 | 'view_count': int, | |
467 | 'like_count': int, | |
468 | 'comment_count': int, | |
469 | }, | |
470 | }, { | |
af1fa623 | 471 | 'url': 'http://music.163.com/#/mv?id=415350', |
472 | 'info_dict': { | |
473 | 'id': '415350', | |
474 | 'ext': 'mp4', | |
475 | 'title': '이럴거면 그러지말지', | |
476 | 'description': '白雅言自作曲唱甜蜜爱情', | |
7a03f88c | 477 | 'creators': ['白娥娟'], |
af1fa623 | 478 | 'upload_date': '20150520', |
f980df73 | 479 | 'thumbnail': r're:http.*\.jpg', |
480 | 'duration': 216, | |
481 | 'view_count': int, | |
482 | 'like_count': int, | |
483 | 'comment_count': int, | |
af1fa623 | 484 | }, |
7a03f88c | 485 | 'skip': 'Blocked outside Mainland China', |
486 | }, { | |
487 | 'note': 'This MV has multiple creators.', | |
488 | 'url': 'https://music.163.com/#/mv?id=22593543', | |
489 | 'info_dict': { | |
490 | 'id': '22593543', | |
491 | 'ext': 'mp4', | |
492 | 'title': '老北京杀器', | |
493 | 'creators': ['秃子2z', '辉子', 'Saber梁维嘉'], | |
494 | 'duration': 206, | |
495 | 'upload_date': '20240618', | |
496 | 'like_count': int, | |
497 | 'comment_count': int, | |
498 | 'thumbnail': r're:http.*\.jpg', | |
499 | 'view_count': int, | |
500 | }, | |
f980df73 | 501 | }] |
af1fa623 | 502 | |
503 | def _real_extract(self, url): | |
504 | mv_id = self._match_id(url) | |
505 | ||
7a03f88c | 506 | info = self._query_api( |
f980df73 | 507 | f'mv/detail?id={mv_id}&type=mp4', mv_id, 'Downloading mv info')['data'] |
af1fa623 | 508 | |
509 | formats = [ | |
f980df73 | 510 | {'url': mv_url, 'ext': 'mp4', 'format_id': f'{brs}p', 'height': int_or_none(brs)} |
af1fa623 | 511 | for brs, mv_url in info['brs'].items() |
512 | ] | |
af1fa623 | 513 | |
514 | return { | |
515 | 'id': mv_id, | |
af1fa623 | 516 | 'formats': formats, |
7a03f88c | 517 | 'creators': traverse_obj(info, ('artists', ..., 'name')) or [info.get('artistName')], |
f980df73 | 518 | **traverse_obj(info, { |
519 | 'title': ('name', {str}), | |
520 | 'description': (('desc', 'briefDesc'), {str}, {lambda x: x or None}), | |
f980df73 | 521 | 'upload_date': ('publishTime', {unified_strdate}), |
522 | 'thumbnail': ('cover', {url_or_none}), | |
7a03f88c | 523 | 'duration': ('duration', {self._kilo_or_none}), |
f980df73 | 524 | 'view_count': ('playCount', {int_or_none}), |
525 | 'like_count': ('likeCount', {int_or_none}), | |
526 | 'comment_count': ('commentCount', {int_or_none}), | |
527 | }, get_all=False), | |
af1fa623 | 528 | } |
529 | ||
530 | ||
531 | class NetEaseMusicProgramIE(NetEaseMusicBaseIE): | |
532 | IE_NAME = 'netease:program' | |
ed848087 | 533 | IE_DESC = '网易云音乐 - 电台节目' |
7a03f88c | 534 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?program\?id=(?P<id>[0-9]+)' |
af1fa623 | 535 | _TESTS = [{ |
536 | 'url': 'http://music.163.com/#/program?id=10109055', | |
537 | 'info_dict': { | |
f980df73 | 538 | 'id': '32593346', |
af1fa623 | 539 | 'ext': 'mp3', |
540 | 'title': '不丹足球背后的故事', | |
541 | 'description': '喜马拉雅人的足球梦 ...', | |
7a03f88c | 542 | 'creators': ['大话西藏'], |
f980df73 | 543 | 'timestamp': 1434179287, |
af1fa623 | 544 | 'upload_date': '20150613', |
f980df73 | 545 | 'thumbnail': r're:http.*\.jpg', |
af1fa623 | 546 | 'duration': 900, |
547 | }, | |
548 | }, { | |
549 | 'note': 'This program has accompanying songs.', | |
550 | 'url': 'http://music.163.com/#/program?id=10141022', | |
551 | 'info_dict': { | |
552 | 'id': '10141022', | |
f980df73 | 553 | 'title': '滚滚电台的有声节目', |
af1fa623 | 554 | 'description': 'md5:8d594db46cc3e6509107ede70a4aaa3b', |
7a03f88c | 555 | 'creators': ['滚滚电台ORZ'], |
f980df73 | 556 | 'timestamp': 1434450733, |
557 | 'upload_date': '20150616', | |
558 | 'thumbnail': r're:http.*\.jpg', | |
af1fa623 | 559 | }, |
560 | 'playlist_count': 4, | |
561 | }, { | |
562 | 'note': 'This program has accompanying songs.', | |
563 | 'url': 'http://music.163.com/#/program?id=10141022', | |
564 | 'info_dict': { | |
f980df73 | 565 | 'id': '32647209', |
af1fa623 | 566 | 'ext': 'mp3', |
f980df73 | 567 | 'title': '滚滚电台的有声节目', |
af1fa623 | 568 | 'description': 'md5:8d594db46cc3e6509107ede70a4aaa3b', |
7a03f88c | 569 | 'creators': ['滚滚电台ORZ'], |
f980df73 | 570 | 'timestamp': 1434450733, |
af1fa623 | 571 | 'upload_date': '20150616', |
f980df73 | 572 | 'thumbnail': r're:http.*\.jpg', |
573 | 'duration': 1104, | |
af1fa623 | 574 | }, |
575 | 'params': { | |
add96eb9 | 576 | 'noplaylist': True, |
75af5d59 | 577 | }, |
af1fa623 | 578 | }] |
579 | ||
580 | def _real_extract(self, url): | |
581 | program_id = self._match_id(url) | |
582 | ||
7a03f88c | 583 | info = self._query_api( |
f980df73 | 584 | f'dj/program/detail?id={program_id}', program_id, note='Downloading program info')['program'] |
af1fa623 | 585 | |
f980df73 | 586 | metainfo = traverse_obj(info, { |
587 | 'title': ('name', {str}), | |
588 | 'description': ('description', {str}), | |
589 | 'creator': ('dj', 'brand', {str}), | |
590 | 'thumbnail': ('coverUrl', {url_or_none}), | |
7a03f88c | 591 | 'timestamp': ('createTime', {self._kilo_or_none}), |
f980df73 | 592 | }) |
af1fa623 | 593 | |
73f12119 M |
594 | if not self._yes_playlist( |
595 | info['songs'] and program_id, info['mainSong']['id'], playlist_label='program', video_label='song'): | |
7a03f88c | 596 | formats = self._extract_formats(info['mainSong']) |
af1fa623 | 597 | |
598 | return { | |
f980df73 | 599 | 'id': str(info['mainSong']['id']), |
af1fa623 | 600 | 'formats': formats, |
7a03f88c | 601 | 'duration': traverse_obj(info, ('mainSong', 'duration', {self._kilo_or_none})), |
f980df73 | 602 | **metainfo, |
af1fa623 | 603 | } |
604 | ||
f980df73 | 605 | songs = traverse_obj(info, (('mainSong', ('songs', ...)),)) |
606 | return self.playlist_result(self._get_entries(songs), program_id, **metainfo) | |
af1fa623 | 607 | |
608 | ||
609 | class NetEaseMusicDjRadioIE(NetEaseMusicBaseIE): | |
610 | IE_NAME = 'netease:djradio' | |
ed848087 | 611 | IE_DESC = '网易云音乐 - 电台' |
7a03f88c | 612 | _VALID_URL = r'https?://music\.163\.com/(?:#/)?djradio\?id=(?P<id>[0-9]+)' |
af1fa623 | 613 | _TEST = { |
614 | 'url': 'http://music.163.com/#/djradio?id=42', | |
615 | 'info_dict': { | |
616 | 'id': '42', | |
617 | 'title': '声音蔓延', | |
add96eb9 | 618 | 'description': 'md5:c7381ebd7989f9f367668a5aee7d5f08', |
af1fa623 | 619 | }, |
620 | 'playlist_mincount': 40, | |
621 | } | |
622 | _PAGE_SIZE = 1000 | |
623 | ||
624 | def _real_extract(self, url): | |
625 | dj_id = self._match_id(url) | |
626 | ||
f980df73 | 627 | metainfo = {} |
af1fa623 | 628 | entries = [] |
ac668111 | 629 | for offset in itertools.count(start=0, step=self._PAGE_SIZE): |
7a03f88c | 630 | info = self._query_api( |
f980df73 | 631 | f'dj/program/byradio?asc=false&limit={self._PAGE_SIZE}&radioId={dj_id}&offset={offset}', |
632 | dj_id, note=f'Downloading dj programs - {offset}') | |
633 | ||
634 | entries.extend(self.url_result( | |
635 | f'http://music.163.com/#/program?id={program["id"]}', NetEaseMusicProgramIE, | |
636 | program['id'], program.get('name')) for program in info['programs']) | |
637 | if not metainfo: | |
638 | metainfo = traverse_obj(info, ('programs', 0, 'radio', { | |
639 | 'title': ('name', {str}), | |
640 | 'description': ('desc', {str}), | |
641 | })) | |
af1fa623 | 642 | |
643 | if not info['more']: | |
644 | break | |
645 | ||
f980df73 | 646 | return self.playlist_result(entries, dj_id, **metainfo) |