├── .gitignore ├── LICENSE.md ├── NetEaseMusicApi ├── RawApi.py ├── SortedApi.py └── __init__.py ├── README.md ├── README.rst ├── README_EN.md ├── run.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | NetEaseMusicApi.egg-info/* 4 | *.pyc 5 | *.swp 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **The MIT License (MIT)** 2 | 3 | Copyright (c) 2016 LittleCoder ([littlecodersh@Github](https://github.com/littlecodersh)) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /NetEaseMusicApi/RawApi.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | import hashlib, base64, random 3 | import requests, json 4 | import os, sys, time 5 | 6 | __all__ = ['NetEaseMusicApi', 'get_dfsId'] 7 | 8 | DEFAULT_LIMIT = 10 9 | BASE_URL = 'http://music.163.com/api/' 10 | 11 | headers = { 12 | 'Cookie': 'appver=1.5.0.75771', 13 | 'Referer': 'http://music.163.com', 14 | } 15 | 16 | _API = { 17 | 'search': { 18 | 'songs': (1, 'songs'), 19 | 'albums': (10, 'albums'), 20 | 'artists': (100, 'artists'), 21 | 'playlists': (1000, 'playlists'), 22 | 'userprofiles': (1002, 'userprofiles'), 23 | 'mvs': (1004, 'mvs'), 24 | 'lyric': (1006, 'songs'), 25 | }, 26 | 'download': '', 27 | 'song': { 28 | 'detail': ('/?id={0}&ids=%5B{0}%5D', 'songs'), 29 | }, 30 | 'artist': { 31 | 'albums': ('/{0}?id={0}&limit={1}', 'hotAlbums'), 32 | }, 33 | 'album': ('/{0}', 'album/songs'), 34 | 'playlist': { 35 | 'detail': ('?id={0}', 'result'), 36 | }, 37 | } 38 | 39 | def _APIProxy(key, value, chain): 40 | if isinstance(value, dict): 41 | childrenList = value.keys() 42 | return lambda:'%s has %s sub functions: %s'%(key, len(childrenList), ', '.join(childrenList)) 43 | else: 44 | def __APIProxy(nameOrId, limit = DEFAULT_LIMIT): 45 | def _get_value(json, keyChain): 46 | for k in keyChain.split('/'): 47 | try: 48 | try: 49 | k = int(k) 50 | except: 51 | pass 52 | json = json[k] 53 | except: 54 | return 55 | return json 56 | if chain[0] == 'search': 57 | url = BASE_URL + '/'.join(chain[:-1] + ['get']) 58 | data = { 59 | 's': nameOrId, 60 | 'type': value[0], 61 | 'offset': 0, 62 | 'sub': 'false', 63 | 'limit': limit, 64 | } 65 | j = requests.post(url, data, headers = headers).json() 66 | return _get_value(j, 'result/' + value[1]) 67 | elif chain[0] == 'download': 68 | url = 'http://m%d.music.126.net/%s/%s.mp3'%(random.randrange(1, 3), encrypted_id(nameOrId), nameOrId) 69 | r = requests.get(url, headers = headers) 70 | return r.content 71 | else: 72 | url = BASE_URL + '/'.join(chain) + value[0].format(nameOrId, limit) 73 | j = requests.get(url, headers = headers).json() 74 | return _get_value(j, value[1]) 75 | return __APIProxy 76 | 77 | def _setup_apiobj(parent, apiList, chain = []): 78 | for k, v in apiList.items(): 79 | setattr(parent, k, _APIProxy(k, v, chain + [k])) 80 | if isinstance(v, dict): _setup_apiobj(getattr(parent, k), v, chain + [k]) 81 | 82 | def encrypted_id(dfsId): 83 | byte1 = bytearray('3go8&$8*3*3h0k(2)2', 'utf8') 84 | byte2 = bytearray(str(dfsId), 'utf8') 85 | byte1_len = len(byte1) 86 | for i in range(len(byte2)): 87 | byte2[i] = byte2[i]^byte1[i%byte1_len] 88 | m = hashlib.md5(byte2).digest() 89 | result = base64.b64encode(m).decode('utf8') 90 | result = result.replace('/', '_') 91 | result = result.replace('+', '-') 92 | return result 93 | 94 | def get_dfsId(song): 95 | dfsId = None 96 | for musicIndex in ('hMusic', 'mMusic', 'lMusic', 'bMusic'): 97 | try: 98 | if not song[musicIndex]['name'] is None: 99 | dfsId = song[musicIndex]['dfsId'] 100 | if not dfsId is None: break 101 | except: 102 | pass 103 | return dfsId 104 | 105 | class NetEaseMusicApi(object): 106 | def __init__(self): 107 | _setup_apiobj(self, _API) 108 | 109 | if __name__ == '__main__': 110 | api = NetEaseMusicApi() 111 | -------------------------------------------------------------------------------- /NetEaseMusicApi/SortedApi.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | import os 3 | from .RawApi import NetEaseMusicApi, get_dfsId 4 | 5 | __all__ = ['save_song', 'save_album', 'interact_select_song'] 6 | 7 | DEFAULT_LIMIT = 10 8 | SINGLE_DETAIL_LENGTH = 10 9 | api = NetEaseMusicApi() 10 | 11 | def _select_index(itemList, detailList, singleDetailLength = 10): 12 | def _get_detail(item, detailList): 13 | valueList = [] 14 | for detail in detailList: 15 | value = item 16 | for key in detail.split('/'): 17 | try: 18 | try: 19 | key = int(key) 20 | except: 21 | pass 22 | value = value[key] 23 | except: 24 | value = '';break 25 | valueList.append(value[:singleDetailLength]) 26 | return '-'.join(valueList) 27 | for i, item in enumerate(itemList): 28 | print(('%-' + str(int(len(itemList)/10) + 4) + 's%s')%( 29 | '[%s]'%(i+1), _get_detail(item, detailList))) 30 | while 1: 31 | try: 32 | selectIndex = int(input('Which one do you want? ')) - 1 33 | if selectIndex < 0 or len(itemList) < selectIndex: raise Exception 34 | break 35 | except: 36 | print('Please input a positive number less than %s'%(len(itemList)+1)) 37 | return selectIndex 38 | 39 | def search_album_id_by_name(albumName, number=DEFAULT_LIMIT): 40 | r = api.search.albums(albumName, number) 41 | if r is None: print('No album named %s'%albumName);return 42 | return r[_select_index(r, ['name', 'artist/name'])]['id'] 43 | 44 | def search_song_id_by_name(songName, number=DEFAULT_LIMIT): 45 | r = api.search.songs(songName, number) 46 | if r is None: print('No song named %s'%songName);return 47 | return r[_select_index(r, ['name', 'artists/0/name', 'album/name'])]['id'] 48 | 49 | def save_song(songName, folder='.', candidateNumber=DEFAULT_LIMIT): 50 | songId = search_song_id_by_name(songName, candidateNumber) 51 | if not songId: return 52 | song = api.song.detail(songId)[0] 53 | if not os.path.exists(folder): os.mkdir(folder) 54 | dfsId = get_dfsId(song) 55 | if dfsId is None: 56 | print('%s.mp3 is sadly lost on the server'%song['name']) 57 | else: 58 | with open(os.path.join(folder, song['name'] + '.mp3'), 'wb') as f: 59 | f.write(api.download(dfsId)) 60 | print('%s.mp3 is downloaded successfully in "%s"'%(song['name'], folder)) 61 | 62 | def save_album(albumName, folder='.', candidateNumber=DEFAULT_LIMIT): 63 | albumId = search_album_id_by_name(albumName, candidateNumber) 64 | if not albumId: return 65 | if not os.path.exists(folder): os.mkdir(folder) 66 | songDir = os.path.join(folder,albumName) 67 | if not os.path.exists(songDir): os.mkdir(songDir) 68 | songs = api.album(albumId) 69 | for song in songs: 70 | print('Downloading %s...'%song['name']) 71 | try: 72 | if get_dfsId(song) is None: raise Exception 73 | with open(os.path.join(songDir, song['name'] + '.mp3'), 'wb') as f: 74 | f.write(api.download(get_dfsId(song))) 75 | except: 76 | print('%s download failed'%song['name']) 77 | print('%s is downloaded successfully in "%s"'%(albumName, songDir)) 78 | 79 | def _search_song_id_by_name(number=DEFAULT_LIMIT, singleDetailLength=SINGLE_DETAIL_LENGTH): 80 | def _get_detail(item, detailList): 81 | valueList = [] 82 | for detail in detailList: 83 | value = item 84 | for key in detail.split('/'): 85 | try: 86 | try: 87 | key = int(key) 88 | except: 89 | pass 90 | value = value[key] 91 | except: 92 | value = '';break 93 | valueList.append(value[:singleDetailLength]) 94 | return '-'.join(valueList) 95 | while 1: 96 | songName = yield 97 | itemList = api.search.songs(songName, number) 98 | if itemList is None: 99 | yield; continue 100 | else: 101 | candidatesList = [] 102 | for i, item in enumerate(itemList): 103 | candidatesList.append(('%-' + str(int(len(itemList)/10 + 4)) + 's%s')%( 104 | '[%s]'%(i+1), _get_detail(item, ['name', 'artists/0/name', 'album/name']))) 105 | yield '\n'.join(candidatesList) 106 | selectIndex = yield 107 | try: 108 | selectIndex = int(selectIndex) - 1 109 | if selectIndex < 0 or len(itemList) < selectIndex: raise Exception 110 | except: 111 | yield; continue 112 | yield itemList[selectIndex]['id'] 113 | ssibn = _search_song_id_by_name() 114 | next(ssibn) 115 | def search_song_id_by_name_interact(msgInput): 116 | r = ssibn.send(msgInput) 117 | next(ssibn) 118 | return r 119 | def _interact_select_song(folder='.'): 120 | if not os.path.exists(folder): os.mkdir(folder) 121 | while 1: 122 | songName = yield 123 | songCandidates = search_song_id_by_name_interact(songName) 124 | if songCandidates: 125 | yield songCandidates 126 | else: 127 | yield u'没有找到%s。'%songName 128 | continue 129 | selectIndex = yield 130 | songId = search_song_id_by_name_interact(selectIndex) 131 | if songId: 132 | song = api.song.detail(songId)[0] 133 | songDir = os.path.join(folder, song['name'] + '.mp3') 134 | dfsId = get_dfsId(song) 135 | if dfsId is None: 136 | yield u'抱歉,该歌曲目前无法提供' 137 | else: 138 | with open(songDir, 'wb') as f: f.write(api.download(dfsId)) 139 | os.startfile(songDir) 140 | yield u'%s 正在播放'%songName 141 | else: 142 | yield u'无效选项,请重新搜索' 143 | iss = _interact_select_song() 144 | next(iss) 145 | def interact_select_song(msgInput): 146 | r = iss.send(msgInput) 147 | next(iss) 148 | return r 149 | 150 | if __name__ == '__main__': 151 | # save_album(u'孤岛') 152 | # save_song(u'南山南') 153 | interact_select_song(u'南山南') 154 | 155 | -------------------------------------------------------------------------------- /NetEaseMusicApi/__init__.py: -------------------------------------------------------------------------------- 1 | from .RawApi import * 2 | from .SortedApi import * 3 | 4 | __all__ = ['api', 'save_song', 'save_album', 'interact_select_song', 'get_dfsId'] 5 | 6 | api = RawApi.NetEaseMusicApi() 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetEaseMusicApi 2 | 3 | ![python](https://img.shields.io/badge/python-2.7-ff69b4.svg) ![python](https://img.shields.io/badge/python-3.5-red.svg) [English Version](https://github.com/littlecodersh/NetEaseMusicApi/blob/master/README_EN.md) 4 | 5 | 完善的网易云音乐Api 6 | 7 | ## 功能简述 8 | * api类提供了常用api的调用,根据`/api/`后的网址调用。 9 | ```python 10 | # 例如http://music.163.com/api/song/detail的调用 11 | # /api/song/detail -> api.song.detail 12 | # songId = 28377211 13 | api.song.detail(28377211) 14 | ``` 15 | * 提供了基本的下载歌曲与专辑作为api示例。 16 | 17 | ## 安装方法 18 | 19 | ```bash 20 | pip install NetEaseMusicApi 21 | ``` 22 | 23 | ## 使用示例 24 | 25 | ```python 26 | from NetEaseMusicApi import api, save_song, save_album 27 | save_song('Apologize') 28 | ``` 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | NetEaseMusicApi 2 | =============== 3 | 4 | |Python2| |Python3| 5 | 6 | Complete api for NetEase Cloud Music 7 | 8 | **Usage** 9 | 10 | - api class provide common api uses, whose function based on url after '/api/'. 11 | 12 | .. code:: python 13 | 14 | # take the api of http://music.163.com/api/song/detail as example 15 | # /api/song/detail -> api.song.detail 16 | # songId = 28377211 17 | api.song.detail(28377211) 18 | 19 | - provide basic functions like save songs and save albums as demo of api 20 | 21 | **Installation** 22 | 23 | .. code:: bash 24 | 25 | pip install NetEaseMusicApi 26 | 27 | **Demo** 28 | 29 | .. code:: python 30 | 31 | from NetEaseMusicApi import api, save_song, save_album 32 | save_song('Apologize') 33 | 34 | .. |Python2| image:: https://img.shields.io/badge/python-2.7-ff69b4.svg 35 | .. |Python3| image:: https://img.shields.io/badge/python-3.5-red.svg 36 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # NetEaseMusicApi 2 | 3 | ![python](https://img.shields.io/badge/python-2.7-ff69b4.svg) ![python](https://img.shields.io/badge/python-3.5-red.svg) [Chinese Version](https://github.com/littlecodersh/NetEaseMusicApi/blob/master/README.md) 4 | 5 | Complete api for NetEase Cloud Music 6 | 7 | ## Usage 8 | * api class provide common api uses, whose function based on url after '/api/'. 9 | ```python 10 | # 例如http://music.163.com/api/song/detail的调用 11 | # /api/song/detail -> api.song.detail 12 | # songId = 28377211 13 | api.song.detail(28377211) 14 | ``` 15 | * provide basic functions like save songs and save albums as demo of api 16 | 17 | ## Installation 18 | 19 | ```bash 20 | pip install NetEaseMusicApi 21 | ``` 22 | 23 | ## Demo 24 | 25 | ```python 26 | from NetEaseMusicApi import api, save_song, save_album 27 | save_song('Apologize') 28 | ``` 29 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from NetEaseMusicApi import api, save_song, save_album, interact_select_song 4 | 5 | save_song('Apologize') 6 | # try: 7 | # sys_input = raw_input 8 | # except: 9 | # sys_input = input 10 | # while 1: 11 | # msg = sys_input('>').encode(sys.stdin.encoding).decode(sys.stdin.encoding) 12 | # print(interact_select_song(msg)) 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ A NetEase cloud music api project 2 | See: 3 | https://github.com/littlecodersh/NetEaseMusicApi 4 | """ 5 | 6 | from setuptools import setup, find_packages 7 | from codecs import open 8 | from os import path 9 | 10 | here = path.abspath(path.dirname(__file__)) 11 | 12 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 13 | long_description = f.read() 14 | 15 | setup( 16 | name='NetEaseMusicApi', 17 | 18 | version='1.0.3', 19 | 20 | description='A complete NetEase cloud music api', 21 | long_description=long_description, 22 | 23 | url='https://github.com/littlecodersh/NetEaseMusicApi', 24 | 25 | author='LittleCoder', 26 | author_email='i7meavnktqegm1b@qq.com', 27 | 28 | license='MIT', 29 | 30 | classifiers=[ 31 | 'Development Status :: 3 - Alpha', 32 | 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Libraries :: Python Modules', 35 | 36 | 'License :: OSI Approved :: MIT License', 37 | 38 | 'Programming Language :: Python :: 2', 39 | 'Programming Language :: Python :: 2.6', 40 | 'Programming Language :: Python :: 2.7', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.3', 43 | 'Programming Language :: Python :: 3.4', 44 | 'Programming Language :: Python :: 3.5', 45 | ], 46 | 47 | keywords='NetEase music api python', 48 | 49 | # You can just specify the packages manually here if your project is 50 | # simple. Or you can use find_packages(). 51 | packages=['NetEaseMusicApi',], 52 | 53 | install_requires=['requests'], 54 | 55 | # List additional groups of dependencies here 56 | extras_require={}, 57 | ) 58 | --------------------------------------------------------------------------------