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