logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

neteasemusic.py (25100B)


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