├── .gitignore ├── LICENSE ├── README.md ├── netease ├── __init__.py ├── compat.py ├── config.py ├── download.py ├── encrypt.py ├── exceptions.py ├── logger.py ├── models.py ├── start.py ├── utils.py └── weapi.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2017 https://github.com/ziwenxie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 一个基于命令行的网易云音乐下载器。 2 | 3 | 4 | ## 安装 5 | 6 | 7 | ### Git clone最新版 8 | 9 | ```bash 10 | $ git clone https://github.com/ziwenxie/netease-dl 11 | $ python3 setup.py install 12 | ``` 13 | 14 | ### PyPi安装 15 | 16 | ```bash 17 | $ pip3 install netease-dl 18 | ``` 19 | 20 | p.s: 仅支持Python3.x。 21 | 22 | 23 | ## 功能特性 24 | 25 | 26 | 通过`--help`可以查看到所有的功能特性,包括下载单首歌曲,下载一张唱片的所有歌曲,下载一个歌手的前50首热门歌曲,下载一张歌单的所有歌曲,下载一个用户的公开歌单以及登录后可下载个人的私人歌单。 27 | 28 | ``` 29 | $ netease-dl --help 30 | Usage: netease-dl [OPTIONS] COMMAND [ARGS]... 31 | 32 | A command tool to download NetEase-Music's songs. 33 | 34 | Options: 35 | -t, --timeout INTEGER Time to wait before giving up, in seconds. 36 | -p, --proxy TEXT Use the specified HTTP/HTTPS/SOCKS proxy. 37 | -o, --output PATH Specify the storage path. 38 | -q, --quiet Automatically select the best one. 39 | -l, --lyric Download lyric. 40 | -a, --again Login Again. 41 | --help Show this message and exit. 42 | 43 | Commands: 44 | album Download a album's songs by name or id. 45 | artist Download a artist's hot songs by name or id. 46 | me Download my playlists. 47 | playlist Download a playlist's songs by id. 48 | song Download a song by name or id. 49 | user Download a user's playlists by id. 50 | ``` 51 | 52 | 53 | ## 使用 54 | 55 | ### 下载单首歌曲 56 | 57 | 使用`song`命令,在后面通过`--name`或者`-n`选项来指定歌曲的名字: 58 | 59 | ``` 60 | $ netease-dl song --name 歌曲名 61 | ``` 62 | 63 | 上面会返回10条搜索结果,可以在`song`命令前面加一个`--quiet`,`netease-dl`会自动匹配第一个返回的结果: 64 | ``` 65 | $ netease-dl --quiet song --name 歌曲名 66 | ``` 67 | 68 | 如果知道歌曲id的话,也可以直接使用`--id`或者`-i`选项来指定: 69 | ``` 70 | $ netease-dl song --id 歌曲id 71 | ``` 72 | 73 | `netease-dl`的所有子命令所支持的特性都可以通过在子命令后面加一个`--help`选项来查看: 74 | ``` 75 | $ netease-dl song --help 76 | Usage: netease-dl song [OPTIONS] 77 | 78 | Download a song by name or id. 79 | 80 | Options: 81 | -n, --name TEXT Song name. 82 | -i, --id INTEGER Song id. 83 | --help Show this message and exit. 84 | ``` 85 | 86 | 87 | ### 下载一个歌手的50首热门歌曲 88 | 89 | 使用`artist`命令,并且在后面通过`--name`或者`-n`选项来指定歌手的姓名: 90 | 91 | ``` 92 | $ netease-dl artist --name 歌手名 93 | ``` 94 | 95 | 和上面下载歌曲的时候一样,也可以使用`--quiet`和`--id`,下面也是一样的原理,接下来我就不重复了。 96 | 97 | 98 | ### 下载一张唱片的所有歌曲 99 | 100 | 使用`album`命令,后面接`--name`或者`-n`选项来指定唱片的名字: 101 | 102 | ``` 103 | $ netease-dl album --name 唱片名 104 | ``` 105 | 106 | 107 | ### 下载一张歌单的所有歌曲 108 | 109 | 使用`playlist`命令,后面接`--name`或者`-n`选项来指定歌单的名字: 110 | ``` 111 | $ netease-dl playlist --name 歌单名 112 | ``` 113 | 114 | ### 下载指定用户的公开歌单 115 | 116 | 使用`user`命令,后面接`--name`或者`-n`选项来指定用户的名字: 117 | ``` 118 | $ netease-dl user --name 用户名 119 | ``` 120 | 121 | 122 | ### 下载个人收藏以及创建的歌单 123 | 124 | 使用`me`命令登录之后可以下载自己的所有歌单包括私人的歌单,以后一段之间之内如果没有修改过密码就不需要重新登录了: 125 | ``` 126 | $ netease-dl me 127 | ``` 128 | 129 | 如果要换一个帐号或者登录密码修改了,使用`--again`或者`-a`选项重新登录: 130 | ``` 131 | $ netease-dl --again me 132 | ``` 133 | 134 | ## 更多选项 135 | 136 | 除了上面提到的`--quiet`选项,正如使用`netease-dl --help`选项看到的,`netease-dl`还支持设置代理,设置超时时间,指定下载目录,是否下载歌词等选项,这些都可以通过在子命令前面加上相关的选项来指定。 137 | 138 | ### 将歌曲下载到指定路径 139 | 140 | 使用`--output`或者`-o`选项指定下载路径: 141 | ``` 142 | $ netease-dl -o 路径名 artist -n 歌手名 143 | ``` 144 | 145 | ### 设置代理 146 | 147 | 海外用户可能要设置相关的代理,`netease-dl`同时支持http和socks协议代理,可以通过`--proxy`或者`-p`选项指定,注意要声明代理所使用的协议: 148 | ``` 149 | $ netease-dl -p 'http://127.0.0.1:8118' artist -n 歌手名 150 | $ netease-dl -p 'socks5://127.0.0.1:1080' artist -n 歌手名 151 | ``` 152 | 153 | ## 更新日志 154 | 155 | 2017-03-19 1.0.2 fix song may contains special character and won't download again if song exists(#2, #3) 156 | 157 | 2017-03-16 1.0.1 fix dependencies problem(#1) 158 | 159 | 160 | ## Contact 161 | 162 | Email: ziwenxiecat@gmail.com 163 | Blog: www.ziwenxie.site 164 | 165 | ## License 166 | 167 | https://github.com/ziwenxie/netease-dl/blob/master/LICENSE 168 | -------------------------------------------------------------------------------- /netease/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4his2/netease-dl/84b226fc07b10f7f66580f0fc69f10356f66b5c3/netease/__init__.py -------------------------------------------------------------------------------- /netease/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.compat 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | This module handles import compatibility issues between Python 2 and 8 | Python 3. 9 | """ 10 | 11 | import sys 12 | 13 | 14 | # Syntax sugar. 15 | _ver = sys.version_info 16 | 17 | #: Python 2.x? 18 | is_py2 = (_ver[0] == 2) 19 | 20 | #: Python 3.x? 21 | is_py3 = (_ver[0] == 3) 22 | 23 | 24 | if is_py2: 25 | import cookielib 26 | 27 | elif is_py3: 28 | from http import cookiejar as cookielib 29 | -------------------------------------------------------------------------------- /netease/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.config 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | Configuration file. 8 | """ 9 | import os 10 | 11 | 12 | conf_dir = os.path.join(os.path.expanduser('~'), '.netease-dl') 13 | person_info_path = os.path.join(conf_dir, 'person_info.json') 14 | cookie_path = os.path.join(conf_dir, 'cookie') 15 | log_path = os.path.join(conf_dir, 'logger.log') 16 | 17 | headers = { 18 | # 'Cookie': 'appver=1.5.2', 19 | 'Accept': '*/*', 20 | 'Accept-Encoding': 'gzip,deflate,sdch', 21 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 22 | 'Connection': 'keep-alive', 23 | 'Content-Type': 'application/x-www-form-urlencoded', 24 | 'Host': 'music.163.com', 25 | 'Referer': 'http://music.163.com/search/', 26 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/56.0.2924.76 Chrome/56.0.2924.76 Safari/537.36' 27 | } 28 | 29 | modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 30 | nonce = '0CoJUm6Qyw8W8jud' 31 | pub_key = '010001' 32 | -------------------------------------------------------------------------------- /netease/download.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.download 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module provides a NetEase class to directly support download operation. 8 | """ 9 | import os 10 | import time 11 | import re 12 | import sys 13 | 14 | import click 15 | from requests.exceptions import RequestException 16 | 17 | from .weapi import Crawler 18 | from .config import person_info_path, cookie_path 19 | from .logger import get_logger 20 | 21 | 22 | LOG = get_logger(__name__) 23 | 24 | 25 | def timeit(method): 26 | """Compute the download time.""" 27 | 28 | def wrapper(*args, **kwargs): 29 | start = time.time() 30 | result = method(*args, **kwargs) 31 | end = time.time() 32 | 33 | click.echo('Cost {}s'.format(int(end-start))) 34 | return result 35 | 36 | return wrapper 37 | 38 | 39 | def login(method): 40 | """Require user to login.""" 41 | 42 | def wrapper(*args, **kwargs): 43 | crawler = args[0].crawler # args[0] is a NetEase object 44 | 45 | try: 46 | if os.path.isfile(cookie_path): 47 | with open(cookie_path, 'r') as cookie_file: 48 | cookie = cookie_file.read() 49 | expire_time = re.compile(r'\d{4}-\d{2}-\d{2}').findall(cookie) 50 | now = time.strftime('%Y-%m-%d', time.localtime(time.time())) 51 | if expire_time[0] > now: 52 | crawler.session.cookies.load() 53 | else: 54 | crawler.login() 55 | else: 56 | crawler.login() 57 | except RequestException: 58 | click.echo('Maybe password error, please try again.') 59 | sys.exit(1) 60 | result = method(*args, **kwargs) 61 | return result 62 | 63 | return wrapper 64 | 65 | 66 | class NetEase(object): 67 | """Provide download operation.""" 68 | 69 | def __init__(self, timeout, proxy, folder, quiet, lyric, again): 70 | self.crawler = Crawler(timeout, proxy) 71 | self.folder = '.' if folder is None else folder 72 | self.quiet = quiet 73 | self.lyric = lyric 74 | try: 75 | if again: 76 | self.crawler.login() 77 | except RequestException: 78 | click.echo('Maybe password error, please try again.') 79 | 80 | def download_song_by_search(self, song_name): 81 | """Download a song by its name. 82 | 83 | :params song_name: song name. 84 | """ 85 | 86 | try: 87 | song = self.crawler.search_song(song_name, self.quiet) 88 | except RequestException as exception: 89 | click.echo(exception) 90 | else: 91 | self.download_song_by_id(song.song_id, song.song_name, self.folder) 92 | 93 | def download_song_by_id(self, song_id, song_name, folder='.'): 94 | """Download a song by id and save it to disk. 95 | 96 | :params song_id: song id. 97 | :params song_name: song name. 98 | :params folder: storage path. 99 | """ 100 | 101 | try: 102 | url = self.crawler.get_song_url(song_id) 103 | if self.lyric: 104 | # use old api 105 | lyric_info = self.crawler.get_song_lyric(song_id) 106 | else: 107 | lyric_info = None 108 | song_name = song_name.replace('/', '') 109 | song_name = song_name.replace('.', '') 110 | self.crawler.get_song_by_url(url, song_name, folder, lyric_info) 111 | except RequestException as exception: 112 | click.echo(exception) 113 | 114 | def download_album_by_search(self, album_name): 115 | """Download a album by its name. 116 | 117 | :params album_name: album name. 118 | """ 119 | 120 | try: 121 | album = self.crawler.search_album(album_name, self.quiet) 122 | except RequestException as exception: 123 | click.echo(exception) 124 | else: 125 | self.download_album_by_id(album.album_id, album.album_name) 126 | 127 | @timeit 128 | def download_album_by_id(self, album_id, album_name): 129 | """Download a album by its name. 130 | 131 | :params album_id: album id. 132 | :params album_name: album name. 133 | """ 134 | 135 | try: 136 | # use old api 137 | songs = self.crawler.get_album_songs(album_id) 138 | except RequestException as exception: 139 | click.echo(exception) 140 | else: 141 | folder = os.path.join(self.folder, album_name) 142 | for song in songs: 143 | self.download_song_by_id(song.song_id, song.song_name, folder) 144 | 145 | def download_artist_by_search(self, artist_name): 146 | """Download a artist's top50 songs by his/her name. 147 | 148 | :params artist_name: artist name. 149 | """ 150 | 151 | try: 152 | artist = self.crawler.search_artist(artist_name, self.quiet) 153 | except RequestException as exception: 154 | click.echo(exception) 155 | else: 156 | self.download_artist_by_id(artist.artist_id, artist.artist_name) 157 | 158 | @timeit 159 | def download_artist_by_id(self, artist_id, artist_name): 160 | """Download a artist's top50 songs by his/her id. 161 | 162 | :params artist_id: artist id. 163 | :params artist_name: artist name. 164 | """ 165 | 166 | try: 167 | # use old api 168 | songs = self.crawler.get_artists_hot_songs(artist_id) 169 | except RequestException as exception: 170 | click.echo(exception) 171 | else: 172 | folder = os.path.join(self.folder, artist_name) 173 | for song in songs: 174 | self.download_song_by_id(song.song_id, song.song_name, folder) 175 | 176 | def download_playlist_by_search(self, playlist_name): 177 | """Download a playlist's songs by its name. 178 | 179 | :params playlist_name: playlist name. 180 | """ 181 | 182 | try: 183 | playlist = self.crawler.search_playlist( 184 | playlist_name, self.quiet) 185 | except RequestException as exception: 186 | click.echo(exception) 187 | else: 188 | self.download_playlist_by_id( 189 | playlist.playlist_id, playlist.playlist_name) 190 | 191 | @timeit 192 | def download_playlist_by_id(self, playlist_id, playlist_name): 193 | """Download a playlist's songs by its id. 194 | 195 | :params playlist_id: playlist id. 196 | :params playlist_name: playlist name. 197 | """ 198 | 199 | try: 200 | songs = self.crawler.get_playlist_songs( 201 | playlist_id) 202 | except RequestException as exception: 203 | click.echo(exception) 204 | else: 205 | folder = os.path.join(self.folder, playlist_name) 206 | for song in songs: 207 | self.download_song_by_id(song.song_id, song.song_name, folder) 208 | 209 | def download_user_playlists_by_search(self, user_name): 210 | """Download user's playlists by his/her name. 211 | 212 | :params user_name: user name. 213 | """ 214 | 215 | try: 216 | user = self.crawler.search_user(user_name, self.quiet) 217 | except RequestException as exception: 218 | click.echo(exception) 219 | else: 220 | self.download_user_playlists_by_id(user.user_id) 221 | 222 | def download_user_playlists_by_id(self, user_id): 223 | """Download user's playlists by his/her id. 224 | 225 | :params user_id: user id. 226 | """ 227 | 228 | try: 229 | playlist = self.crawler.get_user_playlists(user_id) 230 | except RequestException as exception: 231 | click.echo(exception) 232 | else: 233 | self.download_playlist_by_id( 234 | playlist.playlist_id, playlist.playlist_name) 235 | 236 | @login 237 | def download_person_playlists(self): 238 | """Download person playlist including private playlist. 239 | 240 | note: login required. 241 | """ 242 | 243 | with open(person_info_path, 'r') as person_info: 244 | user_id = int(person_info.read()) 245 | self.download_user_playlists_by_id(user_id) 246 | -------------------------------------------------------------------------------- /netease/encrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.encrypt 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | This module provides the encrypt way for NetEase-Music's post request. 8 | """ 9 | import os 10 | import base64 11 | import json 12 | import binascii 13 | from Cryptodome.Cipher import AES 14 | 15 | from .config import modulus, nonce, pub_key 16 | 17 | 18 | def encrypted_request(text): 19 | text = json.dumps(text) 20 | sec_key = create_secret_key(16) 21 | enc_text = aes_encrypt(aes_encrypt(text, nonce), sec_key.decode('utf-8')) 22 | enc_sec_key = rsa_encrpt(sec_key, pub_key, modulus) 23 | data = {'params': enc_text, 'encSecKey': enc_sec_key} 24 | return data 25 | 26 | 27 | def aes_encrypt(text, secKey): 28 | pad = 16 - len(text) % 16 29 | text = text + chr(pad) * pad 30 | encryptor = AES.new(secKey.encode('utf-8'), AES.MODE_CBC, b'0102030405060708') 31 | ciphertext = encryptor.encrypt(text.encode('utf-8')) 32 | ciphertext = base64.b64encode(ciphertext).decode('utf-8') 33 | return ciphertext 34 | 35 | 36 | def rsa_encrpt(text, pubKey, modulus): 37 | text = text[::-1] 38 | rs = pow(int(binascii.hexlify(text), 16), int(pubKey, 16), int(modulus, 16)) 39 | return format(rs, 'x').zfill(256) 40 | 41 | 42 | def create_secret_key(size): 43 | return binascii.hexlify(os.urandom(size))[:16] 44 | -------------------------------------------------------------------------------- /netease/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.exceptions 5 | ~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module contains the set of netease-dl's exceptions. 8 | """ 9 | from requests.exceptions import RequestException 10 | 11 | 12 | class SearchNotFound(RequestException): 13 | """Search api return None.""" 14 | 15 | 16 | class SongNotAvailable(RequestException): 17 | """Some songs are not available, for example Taylor Swift's songs.""" 18 | 19 | 20 | class GetRequestIllegal(RequestException): 21 | """Status code is not 200.""" 22 | 23 | 24 | class PostRequestIllegal(RequestException): 25 | """Status code is not 200.""" 26 | -------------------------------------------------------------------------------- /netease/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.logger 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | This module provides a logger. 8 | """ 9 | import os 10 | import logging 11 | 12 | from .config import conf_dir, log_path 13 | 14 | 15 | if not os.path.isdir(conf_dir): 16 | os.mkdir(conf_dir) 17 | 18 | 19 | with open(log_path, 'a+') as f: 20 | f.write('#' * 80) 21 | f.write('\n') 22 | 23 | 24 | def get_logger(name): 25 | """Return a logger with a file handler.""" 26 | logger = logging.getLogger(name) 27 | logger.setLevel(logging.INFO) 28 | 29 | # File output handler 30 | file_handler = logging.FileHandler(log_path) 31 | file_handler.setLevel(logging.INFO) 32 | formatter = logging.Formatter( 33 | '%(asctime)s %(name)12s %(levelname)8s %(lineno)s %(message)s', 34 | datefmt='%m/%d/%Y %I:%M:%S %p') 35 | file_handler.setFormatter(formatter) 36 | 37 | logger.addHandler(file_handler) 38 | return logger 39 | -------------------------------------------------------------------------------- /netease/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.models 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | ORM for database in the future. 8 | """ 9 | 10 | class Song(object): 11 | 12 | def __init__(self, song_id, song_name, artist_id=None, album_id=None, 13 | hot_comments=None, comment_count=None, song_lyric=None, 14 | song_url=None): 15 | self.song_id = song_id 16 | self.song_name = song_name 17 | self.artist_id = artist_id 18 | self.album_id = album_id 19 | self.hot_comments = [] if hot_comments is None else hot_comments 20 | self.comment_count = 0 if comment_count is None else comment_count 21 | self.song_lyric = u'' if song_lyric is None else song_lyric 22 | self.song_url = '' if song_url is None else song_url 23 | 24 | 25 | class Comment(object): 26 | 27 | def __init__(self, comment_id, content, like_count, created_time, 28 | user_id=None): 29 | self.comment_id = comment_id 30 | self.content = content 31 | self.like_count = like_count 32 | self.created_time = created_time 33 | self.user_id = user_id 34 | 35 | 36 | class Album(object): 37 | 38 | def __init__(self, album_id, album_name, artist_id=None, 39 | songs=None, hot_comments=None): 40 | self.album_id = album_id 41 | self.album_name = album_name 42 | self.artist_id = artist_id 43 | self.songs = [] if songs is None else songs 44 | self.hot_comments = [] if hot_comments is None else hot_comments 45 | 46 | def add_song(self, song): 47 | self.songs.append(song) 48 | 49 | 50 | class Artist(object): 51 | 52 | def __init__(self, artist_id, artist_name, hot_songs=None): 53 | self.artist_id = artist_id 54 | self.artist_name = artist_name 55 | self.hot_songs = [] if hot_songs is None else hot_songs 56 | 57 | def add_song(self, song): 58 | self.hot_songs.append(song) 59 | 60 | 61 | class Playlist(object): 62 | 63 | def __init__(self, playlist_id, playlist_name, user_id=None, 64 | songs=None, hot_comments=None): 65 | self.playlist_id = playlist_id 66 | self.playlist_name = playlist_name 67 | self.user_id = user_id 68 | self.songs = [] if songs is None else songs 69 | self.hot_comments = [] if hot_comments is None else hot_comments 70 | 71 | def add_song(self, song): 72 | self.songs.append(song) 73 | 74 | 75 | class User(object): 76 | 77 | def __init__(self, user_id, user_name, songs=None, hot_comments=None): 78 | self.user_id = user_id 79 | self.user_name = user_name 80 | self.songs = [] if songs is None else songs 81 | self.hot_comments = [] if hot_comments is None else hot_comments 82 | 83 | def add_song(self, song): 84 | self.songs.append(song) 85 | -------------------------------------------------------------------------------- /netease/start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.start 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | Entrance of netease-dl. 8 | """ 9 | import signal 10 | import sys 11 | 12 | import click 13 | 14 | from .download import NetEase 15 | from .logger import get_logger 16 | 17 | 18 | LOG = get_logger(__name__) 19 | 20 | 21 | def signal_handler(sign, frame): 22 | """Capture Ctrl+C.""" 23 | LOG.info('%s => %s', sign, frame) 24 | click.echo('Bye') 25 | sys.exit(0) 26 | 27 | 28 | signal.signal(signal.SIGINT, signal_handler) 29 | 30 | 31 | @click.group() 32 | @click.option('-t', '--timeout', type=int, default=60, 33 | help='Time to wait before giving up, in seconds.') 34 | @click.option('-p', '--proxy', help='Use the specified HTTP/HTTPS/SOCKS proxy.') 35 | @click.option('-o', '--output', type=click.Path(exists=True), 36 | help='Specify the storage path.') 37 | @click.option('-q', '--quiet', is_flag=True, help='Automatically select the best one.') 38 | @click.option('-l', '--lyric', is_flag=True, help='Download lyric.') 39 | @click.option('-a', '--again', is_flag=True, help='Login Again.') 40 | @click.pass_context 41 | def cli(ctx, timeout, proxy, output, quiet, lyric, again): 42 | """A command tool to download NetEase-Music's songs.""" 43 | ctx.obj = NetEase(timeout, proxy, output, quiet, lyric, again) 44 | 45 | 46 | @cli.command() 47 | @click.option('-n', '--name', help='Song name.') 48 | @click.option('-i', '--id', type=int, help='Song id.') 49 | @click.pass_obj 50 | def song(netease, name, id): 51 | """Download a song by name or id.""" 52 | if name: 53 | netease.download_song_by_search(name) 54 | 55 | if id: 56 | netease.download_song_by_id(id, 'song'+str(id)) 57 | 58 | 59 | @cli.command() 60 | @click.option('-n', '--name', help='Album name.') 61 | @click.option('-i', '--id', type=int, help='Album id.') 62 | @click.pass_obj 63 | def album(netease, name, id): 64 | """Download a album's songs by name or id.""" 65 | if name: 66 | netease.download_album_by_search(name) 67 | 68 | if id: 69 | netease.download_album_by_id(id, 'album'+str(id)) 70 | 71 | 72 | @cli.command() 73 | @click.option('-n', '--name', help='Artist name.') 74 | @click.option('-i', '--id', type=int, help='Artist id.') 75 | @click.pass_obj 76 | def artist(netease, name, id): 77 | """Download a artist's hot songs by name or id.""" 78 | if name: 79 | netease.download_artist_by_search(name) 80 | 81 | if id: 82 | netease.download_artist_by_id(id, 'artist'+str(id)) 83 | 84 | 85 | @cli.command() 86 | @click.option('-n', '--name', help='Playlist name.') 87 | @click.option('-i', '--id', type=int, help='Playlist id.') 88 | @click.pass_obj 89 | def playlist(netease, name, id): 90 | """Download a playlist's songs by id.""" 91 | if name: 92 | netease.download_playlist_by_search(name) 93 | 94 | if id: 95 | netease.download_playlist_by_id(id, 'playlist'+str(id)) 96 | 97 | 98 | @cli.command() 99 | @click.option('-n', '--name', help='User name.') 100 | @click.option('-i', '--id', type=int, help='User id.') 101 | @click.pass_obj 102 | def user(netease, name, id): 103 | """Download a user\'s playlists by id.""" 104 | if name: 105 | netease.download_user_playlists_by_search(name) 106 | 107 | if id: 108 | netease.download_user_playlists_by_id(id) 109 | 110 | 111 | @cli.command() 112 | @click.pass_obj 113 | def me(netease): 114 | """Download my playlists.""" 115 | netease.download_person_playlists() 116 | -------------------------------------------------------------------------------- /netease/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.util 5 | ~~~~~~~~~~~~~~~ 6 | 7 | This module provides a Display class to show results to user. 8 | """ 9 | import click 10 | from prettytable import PrettyTable 11 | 12 | from .models import Song, Album, Artist, Playlist, User 13 | 14 | 15 | class Display(object): 16 | """Display the result in the terminal.""" 17 | 18 | @staticmethod 19 | def select_one_song(songs): 20 | """Display the songs returned by search api. 21 | 22 | :params songs: API['result']['songs'] 23 | :return: a Song object. 24 | """ 25 | 26 | if len(songs) == 1: 27 | select_i = 0 28 | else: 29 | table = PrettyTable(['Sequence', 'Song Name', 'Artist Name']) 30 | for i, song in enumerate(songs, 1): 31 | table.add_row([i, song['name'], song['ar'][0]['name']]) 32 | click.echo(table) 33 | 34 | select_i = click.prompt('Select one song', type=int, default=1) 35 | while select_i < 1 or select_i > len(songs): 36 | select_i = click.prompt('Error Select! Select Again', type=int) 37 | 38 | song_id, song_name = songs[select_i-1]['id'], songs[select_i-1]['name'] 39 | song = Song(song_id, song_name) 40 | return song 41 | 42 | @staticmethod 43 | def select_one_album(albums): 44 | """Display the albums returned by search api. 45 | 46 | :params albums: API['result']['albums'] 47 | :return: a Album object. 48 | """ 49 | 50 | if len(albums) == 1: 51 | select_i = 0 52 | else: 53 | table = PrettyTable(['Sequence', 'Album Name', 'Artist Name']) 54 | for i, album in enumerate(albums, 1): 55 | table.add_row([i, album['name'], album['artist']['name']]) 56 | click.echo(table) 57 | 58 | select_i = click.prompt('Select one album', type=int, default=1) 59 | while select_i < 1 or select_i > len(albums): 60 | select_i = click.prompt('Error Select! Select Again', type=int) 61 | 62 | album_id = albums[select_i-1]['id'] 63 | album_name = albums[select_i-1]['name'] 64 | album = Album(album_id, album_name) 65 | return album 66 | 67 | @staticmethod 68 | def select_one_artist(artists): 69 | """Display the artists returned by search api. 70 | 71 | :params artists: API['result']['artists'] 72 | :return: a Artist object. 73 | """ 74 | 75 | if len(artists) == 1: 76 | select_i = 0 77 | else: 78 | table = PrettyTable(['Sequence', 'Artist Name']) 79 | for i, artist in enumerate(artists, 1): 80 | table.add_row([i, artist['name']]) 81 | click.echo(table) 82 | 83 | select_i = click.prompt('Select one artist', type=int, default=1) 84 | while select_i < 1 or select_i > len(artists): 85 | select_i = click.prompt('Error Select! Select Again', type=int) 86 | 87 | artist_id = artists[select_i-1]['id'] 88 | artist_name = artists[select_i-1]['name'] 89 | artist = Artist(artist_id, artist_name) 90 | return artist 91 | 92 | @staticmethod 93 | def select_one_playlist(playlists): 94 | """Display the playlists returned by search api or user playlist. 95 | 96 | :params playlists: API['result']['playlists'] or API['playlist'] 97 | :return: a Playlist object. 98 | """ 99 | 100 | if len(playlists) == 1: 101 | select_i = 0 102 | else: 103 | table = PrettyTable(['Sequence', 'Name']) 104 | for i, playlist in enumerate(playlists, 1): 105 | table.add_row([i, playlist['name']]) 106 | click.echo(table) 107 | 108 | select_i = click.prompt('Select one playlist', type=int, default=1) 109 | while select_i < 1 or select_i > len(playlists): 110 | select_i = click.prompt('Error Select! Select Again', type=int) 111 | 112 | playlist_id = playlists[select_i-1]['id'] 113 | playlist_name = playlists[select_i-1]['name'] 114 | playlist = Playlist(playlist_id, playlist_name) 115 | return playlist 116 | 117 | @staticmethod 118 | def select_one_user(users): 119 | """Display the users returned by search api. 120 | 121 | :params users: API['result']['userprofiles'] 122 | :return: a User object. 123 | """ 124 | 125 | if len(users) == 1: 126 | select_i = 0 127 | else: 128 | table = PrettyTable(['Sequence', 'Name']) 129 | for i, user in enumerate(users, 1): 130 | table.add_row([i, user['nickname']]) 131 | click.echo(table) 132 | 133 | select_i = click.prompt('Select one user', type=int, default=1) 134 | while select_i < 1 or select_i > len(users): 135 | select_i = click.prompt('Error Select! Select Again', type=int) 136 | 137 | user_id = users[select_i-1]['userId'] 138 | user_name = users[select_i-1]['nickname'] 139 | user = User(user_id, user_name) 140 | return user 141 | -------------------------------------------------------------------------------- /netease/weapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | netease-dl.weapi 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | This module provides a Crawler class to get NetEase Music API. 8 | """ 9 | import re 10 | import hashlib 11 | import os 12 | import sys 13 | 14 | import click 15 | import requests 16 | from requests.exceptions import RequestException, Timeout, ProxyError 17 | from requests.exceptions import ConnectionError as ConnectionException 18 | 19 | from .compat import cookielib 20 | from .encrypt import encrypted_request 21 | from .utils import Display 22 | from .config import headers, cookie_path, person_info_path 23 | from .logger import get_logger 24 | from .exceptions import ( 25 | SearchNotFound, SongNotAvailable, GetRequestIllegal, PostRequestIllegal) 26 | from .models import Song, Album, Artist, Playlist, User 27 | 28 | 29 | LOG = get_logger(__name__) 30 | 31 | 32 | def exception_handle(method): 33 | """Handle exception raised by requests library.""" 34 | 35 | def wrapper(*args, **kwargs): 36 | try: 37 | result = method(*args, **kwargs) 38 | return result 39 | except ProxyError: 40 | LOG.exception('ProxyError when try to get %s.', args) 41 | raise ProxyError('A proxy error occurred.') 42 | except ConnectionException: 43 | LOG.exception('ConnectionError when try to get %s.', args) 44 | raise ConnectionException('DNS failure, refused connection, etc.') 45 | except Timeout: 46 | LOG.exception('Timeout when try to get %s', args) 47 | raise Timeout('The request timed out.') 48 | except RequestException: 49 | LOG.exception('RequestException when try to get %s.', args) 50 | raise RequestException('Please check out your network.') 51 | 52 | return wrapper 53 | 54 | 55 | class Crawler(object): 56 | """NetEase Music API.""" 57 | 58 | def __init__(self, timeout=60, proxy=None): 59 | self.session = requests.Session() 60 | self.session.headers.update(headers) 61 | self.session.cookies = cookielib.LWPCookieJar(cookie_path) 62 | self.download_session = requests.Session() 63 | self.timeout = timeout 64 | self.proxies = {'http': proxy, 'https': proxy} 65 | 66 | self.display = Display() 67 | 68 | @exception_handle 69 | def get_request(self, url): 70 | """Send a get request. 71 | 72 | warning: old api. 73 | :return: a dict or raise Exception. 74 | """ 75 | 76 | resp = self.session.get(url, timeout=self.timeout, 77 | proxies=self.proxies) 78 | result = resp.json() 79 | if result['code'] != 200: 80 | LOG.error('Return %s when try to get %s', result, url) 81 | raise GetRequestIllegal(result) 82 | else: 83 | return result 84 | 85 | @exception_handle 86 | def post_request(self, url, params): 87 | """Send a post request. 88 | 89 | :return: a dict or raise Exception. 90 | """ 91 | 92 | data = encrypted_request(params) 93 | resp = self.session.post(url, data=data, timeout=self.timeout, 94 | proxies=self.proxies) 95 | result = resp.json() 96 | if result['code'] != 200: 97 | LOG.error('Return %s when try to post %s => %s', 98 | result, url, params) 99 | raise PostRequestIllegal(result) 100 | else: 101 | return result 102 | 103 | def search(self, search_content, search_type, limit=9): 104 | """Search entrance. 105 | 106 | :params search_content: search content. 107 | :params search_type: search type. 108 | :params limit: result count returned by weapi. 109 | :return: a dict. 110 | """ 111 | 112 | url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token=' 113 | params = {'s': search_content, 'type': search_type, 'offset': 0, 114 | 'sub': 'false', 'limit': limit} 115 | result = self.post_request(url, params) 116 | return result 117 | 118 | def search_song(self, song_name, quiet=False, limit=9): 119 | """Search song by song name. 120 | 121 | :params song_name: song name. 122 | :params quiet: automatically select the best one. 123 | :params limit: song count returned by weapi. 124 | :return: a Song object. 125 | """ 126 | 127 | result = self.search(song_name, search_type=1, limit=limit) 128 | 129 | if result['result']['songCount'] <= 0: 130 | LOG.warning('Song %s not existed!', song_name) 131 | raise SearchNotFound('Song {} not existed.'.format(song_name)) 132 | else: 133 | songs = result['result']['songs'] 134 | if quiet: 135 | song_id, song_name = songs[0]['id'], songs[0]['name'] 136 | song = Song(song_id, song_name) 137 | return song 138 | else: 139 | return self.display.select_one_song(songs) 140 | 141 | def search_album(self, album_name, quiet=False, limit=9): 142 | """Search album by album name. 143 | 144 | :params album_name: album name. 145 | :params quiet: automatically select the best one. 146 | :params limit: album count returned by weapi. 147 | :return: a Album object. 148 | """ 149 | 150 | result = self.search(album_name, search_type=10, limit=limit) 151 | 152 | if result['result']['albumCount'] <= 0: 153 | LOG.warning('Album %s not existed!', album_name) 154 | raise SearchNotFound('Album {} not existed'.format(album_name)) 155 | else: 156 | albums = result['result']['albums'] 157 | if quiet: 158 | album_id, album_name = albums[0]['id'], albums[0]['name'] 159 | album = Album(album_id, album_name) 160 | return album 161 | else: 162 | return self.display.select_one_album(albums) 163 | 164 | def search_artist(self, artist_name, quiet=False, limit=9): 165 | """Search artist by artist name. 166 | 167 | :params artist_name: artist name. 168 | :params quiet: automatically select the best one. 169 | :params limit: artist count returned by weapi. 170 | :return: a Artist object. 171 | """ 172 | 173 | result = self.search(artist_name, search_type=100, limit=limit) 174 | 175 | if result['result']['artistCount'] <= 0: 176 | LOG.warning('Artist %s not existed!', artist_name) 177 | raise SearchNotFound('Artist {} not existed.'.format(artist_name)) 178 | else: 179 | artists = result['result']['artists'] 180 | if quiet: 181 | artist_id, artist_name = artists[0]['id'], artists[0]['name'] 182 | artist = Artist(artist_id, artist_name) 183 | return artist 184 | else: 185 | return self.display.select_one_artist(artists) 186 | 187 | def search_playlist(self, playlist_name, quiet=False, limit=9): 188 | """Search playlist by playlist name. 189 | 190 | :params playlist_name: playlist name. 191 | :params quiet: automatically select the best one. 192 | :params limit: playlist count returned by weapi. 193 | :return: a Playlist object. 194 | """ 195 | 196 | result = self.search(playlist_name, search_type=1000, limit=limit) 197 | 198 | if result['result']['playlistCount'] <= 0: 199 | LOG.warning('Playlist %s not existed!', playlist_name) 200 | raise SearchNotFound('playlist {} not existed'.format(playlist_name)) 201 | else: 202 | playlists = result['result']['playlists'] 203 | if quiet: 204 | playlist_id, playlist_name = playlists[0]['id'], playlists[0]['name'] 205 | playlist = Playlist(playlist_id, playlist_name) 206 | return playlist 207 | else: 208 | return self.display.select_one_playlist(playlists) 209 | 210 | def search_user(self, user_name, quiet=False, limit=9): 211 | """Search user by user name. 212 | 213 | :params user_name: user name. 214 | :params quiet: automatically select the best one. 215 | :params limit: user count returned by weapi. 216 | :return: a User object. 217 | """ 218 | 219 | result = self.search(user_name, search_type=1002, limit=limit) 220 | 221 | if result['result']['userprofileCount'] <= 0: 222 | LOG.warning('User %s not existed!', user_name) 223 | raise SearchNotFound('user {} not existed'.format(user_name)) 224 | else: 225 | users = result['result']['userprofiles'] 226 | if quiet: 227 | user_id, user_name = users[0]['userId'], users[0]['nickname'] 228 | user = User(user_id, user_name) 229 | return user 230 | else: 231 | return self.display.select_one_user(users) 232 | 233 | def get_user_playlists(self, user_id, limit=1000): 234 | """Get a user's all playlists. 235 | 236 | warning: login is required for private playlist. 237 | :params user_id: user id. 238 | :params limit: playlist count returned by weapi. 239 | :return: a Playlist Object. 240 | """ 241 | 242 | url = 'http://music.163.com/weapi/user/playlist?csrf_token=' 243 | csrf = '' 244 | params = {'offset': 0, 'uid': user_id, 'limit': limit, 245 | 'csrf_token': csrf} 246 | result = self.post_request(url, params) 247 | playlists = result['playlist'] 248 | return self.display.select_one_playlist(playlists) 249 | 250 | def get_playlist_songs(self, playlist_id, limit=1000): 251 | """Get a playlists's all songs. 252 | 253 | :params playlist_id: playlist id. 254 | :params limit: length of result returned by weapi. 255 | :return: a list of Song object. 256 | """ 257 | 258 | url = 'http://music.163.com/weapi/v3/playlist/detail?csrf_token=' 259 | csrf = '' 260 | params = {'id': playlist_id, 'offset': 0, 'total': True, 261 | 'limit': limit, 'n': 1000, 'csrf_token': csrf} 262 | result = self.post_request(url, params) 263 | 264 | songs = result['playlist']['tracks'] 265 | songs = [Song(song['id'], song['name']) for song in songs] 266 | return songs 267 | 268 | def get_album_songs(self, album_id): 269 | """Get a album's all songs. 270 | 271 | warning: use old api. 272 | :params album_id: album id. 273 | :return: a list of Song object. 274 | """ 275 | 276 | url = 'http://music.163.com/api/album/{}/'.format(album_id) 277 | result = self.get_request(url) 278 | 279 | songs = result['album']['songs'] 280 | songs = [Song(song['id'], song['name']) for song in songs] 281 | return songs 282 | 283 | def get_artists_hot_songs(self, artist_id): 284 | """Get a artist's top50 songs. 285 | 286 | warning: use old api. 287 | :params artist_id: artist id. 288 | :return: a list of Song object. 289 | """ 290 | url = 'http://music.163.com/api/artist/{}'.format(artist_id) 291 | result = self.get_request(url) 292 | 293 | hot_songs = result['hotSongs'] 294 | songs = [Song(song['id'], song['name']) for song in hot_songs] 295 | return songs 296 | 297 | def get_song_url(self, song_id, bit_rate=320000): 298 | """Get a song's download address. 299 | 300 | :params song_id: song id. 301 | :params bit_rate: {'MD 128k': 128000, 'HD 320k': 320000} 302 | :return: a song's download address. 303 | """ 304 | 305 | url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' 306 | csrf = '' 307 | params = {'ids': [song_id], 'br': bit_rate, 'csrf_token': csrf} 308 | result = self.post_request(url, params) 309 | song_url = result['data'][0]['url'] # download address 310 | 311 | if song_url is None: # Taylor Swift's song is not available 312 | LOG.warning( 313 | 'Song %s is not available due to copyright issue. => %s', 314 | song_id, result) 315 | raise SongNotAvailable( 316 | 'Song {} is not available due to copyright issue.'.format(song_id)) 317 | else: 318 | return song_url 319 | 320 | def get_song_lyric(self, song_id): 321 | """Get a song's lyric. 322 | 323 | warning: use old api. 324 | :params song_id: song id. 325 | :return: a song's lyric. 326 | """ 327 | 328 | url = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format( # NOQA 329 | song_id) 330 | result = self.get_request(url) 331 | if 'lrc' in result and result['lrc']['lyric'] is not None: 332 | lyric_info = result['lrc']['lyric'] 333 | else: 334 | lyric_info = 'Lyric not found.' 335 | return lyric_info 336 | 337 | @exception_handle 338 | def get_song_by_url(self, song_url, song_name, folder, lyric_info): 339 | """Download a song and save it to disk. 340 | 341 | :params song_url: download address. 342 | :params song_name: song name. 343 | :params folder: storage path. 344 | :params lyric: lyric info. 345 | """ 346 | 347 | if not os.path.exists(folder): 348 | os.makedirs(folder) 349 | fpath = os.path.join(folder, song_name+'.mp3') 350 | 351 | if sys.platform == 'win32' or sys.platform == 'cygwin': 352 | valid_name = re.sub(r'[<>:"/\\|?*]', '', song_name) 353 | if valid_name != song_name: 354 | click.echo('{} will be saved as: {}.mp3'.format(song_name, valid_name)) 355 | fpath = os.path.join(folder, valid_name + '.mp3') 356 | 357 | if not os.path.exists(fpath): 358 | resp = self.download_session.get( 359 | song_url, timeout=self.timeout, stream=True) 360 | length = int(resp.headers.get('content-length')) 361 | label = 'Downloading {} {}kb'.format(song_name, int(length/1024)) 362 | 363 | with click.progressbar(length=length, label=label) as progressbar: 364 | with open(fpath, 'wb') as song_file: 365 | for chunk in resp.iter_content(chunk_size=1024): 366 | if chunk: # filter out keep-alive new chunks 367 | song_file.write(chunk) 368 | progressbar.update(1024) 369 | 370 | if lyric_info: 371 | folder = os.path.join(folder, 'lyric') 372 | if not os.path.exists(folder): 373 | os.makedirs(folder) 374 | fpath = os.path.join(folder, song_name+'.lrc') 375 | with open(fpath, 'w') as lyric_file: 376 | lyric_file.write(lyric_info) 377 | 378 | def login(self): 379 | """Login entrance.""" 380 | username = click.prompt('Please enter your email or phone number') 381 | password = click.prompt('Please enter your password', hide_input=True) 382 | 383 | pattern = re.compile(r'^0\d{2,3}\d{7,8}$|^1[34578]\d{9}$') 384 | if pattern.match(username): # use phone number to login 385 | url = 'https://music.163.com/weapi/login/cellphone' 386 | params = { 387 | 'phone': username, 388 | 'password': hashlib.md5(password.encode('utf-8')).hexdigest(), 389 | 'rememberLogin': 'true'} 390 | else: # use email to login 391 | url = 'https://music.163.com/weapi/login?csrf_token=' 392 | params = { 393 | 'username': username, 394 | 'password': hashlib.md5(password.encode('utf-8')).hexdigest(), 395 | 'rememberLogin': 'true'} 396 | 397 | try: 398 | result = self.post_request(url, params) 399 | except PostRequestIllegal: 400 | click.echo('Password Error!') 401 | sys.exit(1) 402 | self.session.cookies.save() 403 | uid = result['account']['id'] 404 | with open(person_info_path, 'w') as person_info: 405 | person_info.write(str(uid)) 406 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | @Author: ziwenxie 5 | @Date: 2017-03-04 20:00:30 6 | """ 7 | from setuptools import setup, find_packages 8 | 9 | 10 | setup( 11 | name='netease-dl', 12 | version='1.0.2', 13 | packages=find_packages(), 14 | include_package_data=True, 15 | install_requires=[ 16 | 'requests>=2.10.0', 17 | 'pycryptodomex', 18 | 'click>=5.1', 19 | 'PrettyTable>=0.7.2', 20 | ], 21 | 22 | entry_points=''' 23 | [console_scripts] 24 | netease-dl=netease.start:cli 25 | ''', 26 | 27 | license='MIT', 28 | author='ziwenxie', 29 | author_email='ziwenxiecat@gmail.com', 30 | url='https://github.com/ziwenxie/netease-dl', 31 | description='一个基于命令行的网易云音乐下载器', 32 | keywords=['music', 'netease', 'download', 'command tool'], 33 | ) 34 | --------------------------------------------------------------------------------