├── .coveragerc ├── static ├── fork.png ├── logo.png ├── wepay.jpg ├── advance.png ├── preview.png ├── sample.mp3 └── preview-en.png ├── requirements.txt ├── music_dl ├── addons │ ├── __init__.py │ ├── baidu.py │ ├── xiami.py │ ├── migu.py │ ├── qq.py │ ├── kugou.py │ └── netease.py ├── __init__.py ├── __version__.py ├── exceptions.py ├── utils.py ├── api.py ├── config.py ├── source.py ├── player.py ├── __main__.py └── song.py ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug-report.md ├── tests ├── __init__.py ├── test_addon_qq.py ├── test_addon_baidu.py ├── test_addon_kugou.py ├── test_addon_xiami.py ├── test_addon_netease.py ├── test_main.py ├── test_utils.py ├── test_source.py ├── test_config.py ├── test_song.py └── test_filter.py ├── music-dl ├── .travis.yml ├── Makefile ├── LICENSE ├── .gitignore ├── setup.py ├── locale ├── music-dl.pot ├── de │ └── LC_MESSAGES │ │ └── music-dl.po ├── jp │ └── LC_MESSAGES │ │ └── music-dl.po ├── zh_CN │ └── LC_MESSAGES │ │ └── music-dl.po ├── hr │ └── LC_MESSAGES │ │ └── music-dl.po ├── sr │ └── LC_MESSAGES │ │ └── music-dl.po └── en │ └── LC_MESSAGES │ └── music-dl.po ├── README.en.md └── README.md /.coveragerc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/fork.png -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/wepay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/wepay.jpg -------------------------------------------------------------------------------- /static/advance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/advance.png -------------------------------------------------------------------------------- /static/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/preview.png -------------------------------------------------------------------------------- /static/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/sample.mp3 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | requests 3 | pycryptodome 4 | prettytable 5 | pygame>=2.0.0 6 | -------------------------------------------------------------------------------- /static/preview-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xHJK/music-dl/HEAD/static/preview-en.png -------------------------------------------------------------------------------- /music_dl/addons/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: __init__.py 6 | @time: 2019-06-11 7 | """ 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 欢迎提交新的思路和建议 4 | title: "[Feature]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **思路和建议** 11 | 12 | 13 | 14 | **参考代码** 15 | 提供代码参考 16 | -------------------------------------------------------------------------------- /music_dl/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: __init__.py 6 | @time: 2019-01-26 7 | """ 8 | 9 | from .__version__ import __version__ 10 | from .__main__ import main 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: __init__.py 6 | @time: 2019-01-29 7 | """ 8 | 9 | import gettext 10 | from music_dl import config 11 | 12 | config.init() 13 | gettext.install("music-dl", "locale") 14 | -------------------------------------------------------------------------------- /tests/test_addon_qq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_addon_qq.py 6 | @time: 2019-06-10 7 | """ 8 | 9 | 10 | from music_dl.addons import qq 11 | 12 | 13 | def test_qq(): 14 | songs_list = qq.search("周杰伦") 15 | assert songs_list is not None 16 | -------------------------------------------------------------------------------- /tests/test_addon_baidu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_addon_baidu.py 6 | @time: 2019-06-10 7 | """ 8 | 9 | 10 | from music_dl.addons import baidu 11 | 12 | 13 | def test_baidu(): 14 | songs_list = baidu.search("许巍") 15 | assert songs_list is not None 16 | -------------------------------------------------------------------------------- /tests/test_addon_kugou.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_addon_kugou.py 6 | @time: 2019-06-10 7 | """ 8 | 9 | 10 | from music_dl.addons import kugou 11 | 12 | 13 | def test_kugou(): 14 | songs_list = kugou.search("周杰伦") 15 | assert songs_list is not None 16 | -------------------------------------------------------------------------------- /tests/test_addon_xiami.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_addon_xiami.py 6 | @time: 2019-06-13 7 | """ 8 | 9 | 10 | from music_dl.addons import xiami 11 | 12 | 13 | # def test_xiami(): 14 | # songs_list = xiami.search("好妹妹乐队") 15 | # assert songs_list is not None 16 | -------------------------------------------------------------------------------- /tests/test_addon_netease.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_addon_netease.py 6 | @time: 2019-06-10 7 | """ 8 | 9 | 10 | from music_dl.addons import netease 11 | 12 | 13 | def test_netease(): 14 | songs_list = netease.search("谢春花") 15 | assert songs_list is not None 16 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_main 6 | @time: 2019-01-30 7 | """ 8 | import click 9 | from click.testing import CliRunner 10 | from music_dl import __main__ as m 11 | from music_dl import config 12 | 13 | 14 | def test_run(): 15 | pass 16 | 17 | 18 | def test_main(): 19 | pass 20 | -------------------------------------------------------------------------------- /music_dl/__version__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @author: HJK 5 | @file: __version__.py 6 | @time: 2019-01-28 7 | """ 8 | 9 | __title__ = "pymusic-dl" 10 | __description__ = "Search and download music from netease, qq, kugou, baidu and xiami." 11 | __url__ = "https://github.com/0xHJK/music-dl" 12 | __version__ = "3.0.1" 13 | __author__ = "HJK" 14 | __author_email__ = "HJKdev@gmail.com" 15 | __license__ = "MIT License" 16 | __copyright__ = "Copyright 2019 HJK" 17 | -------------------------------------------------------------------------------- /music-dl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: music-dl 6 | @time: 2019-01-26 7 | 8 | 启动器 9 | 10 | """ 11 | 12 | import os, sys 13 | import gettext 14 | 15 | gettext.install("music-dl", "locale") 16 | 17 | _srcdir = "%s/" % os.path.dirname(os.path.realpath(__file__)) 18 | _filepath = os.path.dirname(sys.argv[0]) 19 | sys.path.insert(1, os.path.join(_filepath, _srcdir)) 20 | 21 | if sys.version_info[0] == 3: 22 | import music_dl 23 | 24 | if __name__ == "__main__": 25 | music_dl.main() 26 | else: # Python 2 27 | print(_("Python3 Only.")) 28 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_utils.py 6 | @time: 2019-01-30 7 | """ 8 | 9 | import platform 10 | from music_dl import utils 11 | 12 | 13 | def test_color(): 14 | if platform.system() == "Windows": 15 | assert utils.colorize("music-dl", "qq") == "music-dl" 16 | assert utils.colorize(1234, "qq") == "1234" 17 | else: 18 | assert utils.colorize("music-dl", "qq") == "\033[92mmusic-dl\033[0m" 19 | assert utils.colorize(1234, "xiami") == "\033[93m1234\033[0m" 20 | assert utils.colorize("music-dl", "fsadfasdg") == "music-dl" 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 使用过程中遇到了问题 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **问题描述(Question description)** 11 | 提交issue前,请先检查是否是最新的代码、环境是否符合要求、依赖是否安装完整 12 | 请尽可能填写完整以下信息,方便排查问题,填写不完整可能不会受理 13 | 解决问题后请关闭issue,谢谢 14 | 15 | - 搜索的关键字(Keyword): 16 | - 出错的音乐源(Music source): 17 | - 使用的命令(Command used): 18 | 19 | **使用的环境(Environment)** 20 | - 安装方式(Installation method): 21 | - music-dl版本(music-dl version): 22 | - 操作系统版本(OS version): 23 | - Python版本(Python version): 24 | 25 | **截图** 26 | 使用`-v`参数运行,截取完整错误信息 27 | Run with the `-v` parameter to get the full error message 28 | 29 | **修改建议** 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://travis-ci.org/0xHJK/music-dl 2 | language: python 3 | 4 | install: 5 | - pip install pytest-cov codecov 6 | - make install 7 | 8 | jobs: 9 | include: 10 | - stage: test 11 | script: 12 | - make test 13 | - make ci 14 | python: '3.5' 15 | - stage: test 16 | script: 17 | - make test 18 | - make ci 19 | python: '3.6' 20 | - stage: test 21 | script: 22 | - make test 23 | - make ci 24 | python: '3.7' 25 | dist: xenial 26 | - stage: coverage 27 | python: '3.6' 28 | script: 29 | - make coverage 30 | - codecov 31 | -------------------------------------------------------------------------------- /tests/test_source.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_source.py 6 | @time: 2019-06-11 7 | """ 8 | 9 | 10 | from music_dl.source import MusicSource 11 | 12 | 13 | def test_search(): 14 | ms = MusicSource() 15 | songs_list = ms.search("五月天", ["baidu"]) 16 | assert songs_list is not None 17 | 18 | 19 | # def test_single(): 20 | # ms = MusicSource() 21 | # song = ms.single("https://music.163.com/#/song?id=26427663") 22 | # assert song is not None 23 | # 24 | # 25 | # def test_playlist(): 26 | # ms = MusicSource() 27 | # songs_list = ms.playlist("https://music.163.com/#/playlist?id=2602222983") 28 | # assert songs_list is not None 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | ci: 4 | py.test --junitxml=report.xml 5 | 6 | test: 7 | python3 setup.py test 8 | 9 | coverage: 10 | py.test --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=music_dl --junitxml=report.xml tests 11 | 12 | flake8: 13 | flake8 --ignore=E501,F401,W503 music_dl 14 | 15 | clean: 16 | rm -fr build dist .egg pymusic_dl.egg-info 17 | rm -fr *.mp3 .pytest_cache coverage.xml report.xml htmlcov 18 | find . | grep __pycache__ | xargs rm -fr 19 | find . | grep .pyc | xargs rm -f 20 | 21 | install: 22 | python3 setup.py install 23 | 24 | publish: 25 | pip3 install 'twine>=1.5.0' 26 | python3 setup.py sdist bdist_wheel 27 | twine upload dist/* 28 | rm -fr build .egg requests.egg-info 29 | -------------------------------------------------------------------------------- /music_dl/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @author: HJK 5 | @file: exceptions.py 6 | @time: 2019-01-09 7 | 8 | 自定义异常 9 | 10 | """ 11 | 12 | 13 | class RequestError(RuntimeError): 14 | """ 请求时的状态码错误 """ 15 | 16 | def __init__(self, *args, **kwargs): 17 | pass 18 | 19 | 20 | class ResponseError(RuntimeError): 21 | """ 得到的response状态错误 """ 22 | 23 | def __init__(self, *args, **kwargs): 24 | pass 25 | 26 | 27 | class DataError(RuntimeError): 28 | """ 得到的data中没有预期的内容 """ 29 | 30 | def __init__(self, *args, **kwargs): 31 | pass 32 | 33 | 34 | class ParameterError(RuntimeError): 35 | """ 输入的参数错误 """ 36 | 37 | def __init__(self, *args, **kwargs): 38 | pass 39 | -------------------------------------------------------------------------------- /music_dl/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @author: HJK 5 | @file: utils.py 6 | @time: 2019-01-28 7 | 8 | 控制台输出内容控制 9 | 10 | """ 11 | import platform 12 | 13 | colors = { 14 | "red": "\033[31m", 15 | "green": "\033[32m", 16 | "yellow": "\033[33m", 17 | "blue": "\033[34m", 18 | "pink": "\033[35m", 19 | "cyan": "\033[36m", 20 | "qq": "\033[92m", 21 | "kugou": "\033[94m", 22 | "netease": "\033[91m", 23 | "baidu": "\033[96m", 24 | "xiami": "\033[93m", 25 | "flac": "\033[95m", 26 | "highlight": "\033[93m", 27 | "error": "\033[31m", 28 | } 29 | 30 | 31 | def colorize(string, color): 32 | string = str(string) 33 | if color not in colors: 34 | return string 35 | if platform.system() == "Windows": 36 | return string 37 | return colors[color] + string + "\033[0m" 38 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_config 6 | @time: 2019-01-30 7 | """ 8 | import pytest 9 | from music_dl import config 10 | 11 | 12 | def test_init(): 13 | # with pytest.raises(AttributeError): 14 | # config.opts 15 | config.init() 16 | assert config.opts 17 | 18 | 19 | def test_get(): 20 | config.init() 21 | assert config.get("number") == 5 22 | assert config.get("outdir") == "." 23 | assert config.get("fasdfjklasd") == "" 24 | 25 | 26 | def test_set(): 27 | config.init() 28 | assert config.get("fasdfjklasd") == "" 29 | config.set("fasdfjklasd", "music-dl") 30 | assert config.get("fasdfjklasd") == "music-dl" 31 | proxies = {"http": "http://127.0.0.1:1087", "https": "http://127.0.0.1:1087"} 32 | config.set("proxies", proxies) 33 | assert config.get("proxies")["http"] == "http://127.0.0.1:1087" 34 | assert config.get("proxies")["https"] == "http://127.0.0.1:1087" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2019 Mort Yao 4 | Copyright (c) 2012 Boyu Guo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/test_song.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: test_song 6 | @time: 2019-05-28 7 | """ 8 | 9 | import os 10 | from music_dl.song import BasicSong 11 | from music_dl import config 12 | 13 | 14 | def test_music(capsys): 15 | config.init() 16 | config.set("outdir", "/tmp") 17 | # config.set("cover", True) 18 | # config.set("lyrics", True) 19 | config.set("verbose", True) 20 | song = BasicSong() 21 | song.id = 816477 22 | song.title = "cheering" 23 | song.singer = "crowd" 24 | song.ext = "mp3" 25 | song.album = "sample" 26 | song.rate = 128 27 | song.source = "sample" 28 | song.duration = 28 29 | song.song_url = "https://github.com/0xHJK/music-dl/raw/master/static/sample.mp3" 30 | 31 | assert song.available 32 | assert song.size == 0.42 33 | assert song.duration == "0:00:28" 34 | 35 | os.system("rm /tmp/*.mp3") 36 | 37 | assert song.song_fullname == "/tmp/crowd - cheering.mp3" 38 | assert song.cover_fullname == "/tmp/crowd - cheering.jpg" 39 | assert song.lyrics_fullname == "/tmp/crowd - cheering.lrc" 40 | 41 | str(song) 42 | 43 | song.download() 44 | out, err = capsys.readouterr() 45 | assert out.find("/tmp/crowd - cheering") 46 | 47 | os.system("rm /tmp/*.mp3") 48 | -------------------------------------------------------------------------------- /music_dl/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: api.py 6 | @time: 2019-06-11 7 | """ 8 | 9 | import requests 10 | from . import config 11 | from .exceptions import RequestError, ResponseError, DataError 12 | 13 | 14 | class MusicApi: 15 | # class property 16 | # 子类修改时使用deepcopy 17 | session = requests.Session() 18 | session.headers.update(config.get("fake_headers")) 19 | if config.get("proxies"): 20 | session.proxies.update(config.get("proxies")) 21 | session.headers.update({"referer": "http://www.google.com/"}) 22 | 23 | @classmethod 24 | def request(cls, url, method="POST", data=None): 25 | if method == "GET": 26 | resp = cls.session.get(url, params=data, timeout=7) 27 | else: 28 | resp = cls.session.post(url, data=data, timeout=7) 29 | if resp.status_code != requests.codes.ok: 30 | raise RequestError(resp.text) 31 | if not resp.text: 32 | raise ResponseError("No response data.") 33 | return resp.json() 34 | 35 | @classmethod 36 | def requestInstance(cls, url, method="POST", data=None): 37 | if method == "GET": 38 | resp = cls.session.get(url, params=data, timeout=7) 39 | else: 40 | resp = cls.session.post(url, data=data, timeout=7) 41 | if resp.status_code != requests.codes.ok: 42 | raise RequestError(resp.text) 43 | if not resp.text: 44 | raise ResponseError("No response data.") 45 | return resp -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 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 | .pytest_cache/ 49 | report.xml 50 | 51 | # Translations 52 | *.mo 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .idea 106 | .vscode 107 | test.py 108 | 109 | # music 110 | *.mp3 111 | *.ogg 112 | *.flac 113 | !static/sample.mp3 114 | 115 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: setup.py 6 | @time: 2019-01-26 7 | 8 | 打包配置文件 9 | 10 | """ 11 | import os 12 | import sys 13 | import setuptools 14 | 15 | # 'setup.py publish' shortcut. 16 | if sys.argv[-1] == "publish": 17 | os.system("rm -rf dist") 18 | os.system("python setup.py sdist bdist_wheel") 19 | os.system("twine upload dist/*") 20 | sys.exit() 21 | 22 | here = os.path.abspath(os.path.dirname(__file__)) 23 | about = {} 24 | with open(os.path.join(here, "music_dl", "__version__.py"), "r", encoding="utf-8") as f: 25 | exec(f.read(), about) 26 | 27 | with open("README.md", "r", encoding="utf-8") as fh: 28 | long_description = fh.read() 29 | 30 | setuptools.setup( 31 | name=about["__title__"], 32 | version=about["__version__"], 33 | description=about["__description__"], 34 | author=about["__author__"], 35 | author_email=about["__author_email__"], 36 | url=about["__url__"], 37 | license=about["__license__"], 38 | long_description=long_description, 39 | long_description_content_type="text/markdown", 40 | packages=setuptools.find_packages(), 41 | test_suite="tests", 42 | entry_points={"console_scripts": ["music-dl = music_dl.__main__:main"]}, 43 | install_requires=["requests", "click", "pycryptodome", "prettytable"], 44 | classifiers=[ 45 | "Development Status :: 4 - Beta", 46 | "Environment :: Console", 47 | "Intended Audience :: Developers", 48 | "Intended Audience :: End Users/Desktop", 49 | "License :: OSI Approved :: MIT License", 50 | "Operating System :: OS Independent", 51 | "Programming Language :: Python", 52 | "Programming Language :: Python :: 3", 53 | "Programming Language :: Python :: 3 :: Only", 54 | "Programming Language :: Python :: 3.5", 55 | "Programming Language :: Python :: 3.6", 56 | "Programming Language :: Python :: 3.7", 57 | "Topic :: Internet", 58 | "Topic :: Internet :: WWW/HTTP", 59 | "Topic :: Multimedia", 60 | "Topic :: Multimedia :: Sound/Audio", 61 | "Topic :: Utilities", 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /music_dl/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @author: HJK 5 | @file: config.py 6 | @time: 2019-01-27 7 | 8 | 全局变量 9 | 10 | """ 11 | 12 | __all__ = ["init", "set", "get"] 13 | 14 | 15 | def init(): 16 | global opts 17 | opts = { 18 | # 自定义来源 -s --source 19 | "source": "baidu netease qq kugou", 20 | # 自定义数量 -n --number 21 | "number": 5, 22 | # 保存目录 -o --outdir 23 | "outdir": ".", 24 | # 搜索关键字 -k --keyword 25 | "keyword": "", 26 | # 从URL下载 -u --url 27 | "url": "", 28 | # 下载歌单 -p --playlist 29 | "playlist": "", 30 | # 代理 -x --proxy 31 | "proxies": None, 32 | # 显示详情 -v --verbose 33 | "verbose": False, 34 | # 搜索结果不排序去重 --nomerge 35 | "nomerge": False, 36 | # 下载歌词 --lyrics 37 | "lyrics": False, 38 | # 下载封面 --cover 39 | "cover": False, 40 | # 下载后播放 --play 41 | "play": False, 42 | # 过滤器,如'size>8,length>300'表示大于8MB且时长超过5分钟 43 | "filter": "", 44 | # 一般情况下的headers 45 | "fake_headers": { 46 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", # noqa 47 | "Accept-Charset": "UTF-8,*;q=0.5", 48 | "Accept-Encoding": "gzip,deflate,sdch", 49 | "Accept-Language": "en-US,en;q=0.8", 50 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:60.0) Gecko/20100101 Firefox/60.0", # noqa 51 | "referer": "https://www.google.com", 52 | }, 53 | # QQ下载音乐不能没有User-Agent 54 | # 百度下载音乐User-Agent不能是浏览器 55 | # 下载时的headers 56 | "wget_headers": { 57 | "Accept": "*/*", 58 | "Accept-Encoding": "identity", 59 | "User-Agent": "Wget/1.19.5 (darwin17.5.0)", 60 | }, 61 | # 移动端useragent 62 | "ios_useragent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46" 63 | + " (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", 64 | } 65 | 66 | 67 | def get(key): 68 | return opts.get(key, "") 69 | 70 | 71 | def set(key, value): 72 | opts[key] = value 73 | -------------------------------------------------------------------------------- /music_dl/addons/baidu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: baidu.py 6 | @time: 2019-05-08 7 | """ 8 | 9 | import copy 10 | from .. import config 11 | from ..api import MusicApi 12 | from ..song import BasicSong 13 | 14 | 15 | class BaiduSong(BasicSong): 16 | def __init__(self): 17 | super(BaiduSong, self).__init__() 18 | 19 | 20 | class BaiduApi(MusicApi): 21 | session = copy.deepcopy(MusicApi.session) 22 | session.headers.update({"referer": "http://music.baidu.com/"}) 23 | 24 | 25 | def baidu_search(keyword) -> list: 26 | """ 搜索音乐 """ 27 | number = config.get("number") or 5 28 | params = dict( 29 | query=keyword, 30 | method="baidu.ting.search.common", 31 | format="json", 32 | page_no=1, 33 | page_size=number, 34 | ) 35 | 36 | songs_list = [] 37 | res_data = BaiduApi.request( 38 | "http://musicapi.qianqian.com/v1/restserver/ting", method="GET", data=params 39 | ).get("song_list", []) 40 | 41 | for item in res_data: 42 | song = BaiduSong() 43 | song.source = "baidu" 44 | song.id = item.get("song_id", "") 45 | song.title = item.get("title", "").replace("", "").replace("", "") 46 | song.singer = item.get("author").replace("", "").replace("", "") 47 | song.album = item.get("album_title").replace("", "").replace("", "") 48 | song.lyrics_url = "http://musicapi.qianqian.com/v1/restserver/ting" + item.get( 49 | "lrclink", "" 50 | ) 51 | 52 | m_params = dict(method="baidu.ting.song.play", bit=320, songid=song.id) 53 | res_song_data = BaiduApi.request( 54 | "http://tingapi.ting.baidu.com/v1/restserver/ting", 55 | method="GET", 56 | data=m_params, 57 | ) 58 | 59 | bitrate = res_song_data.get("bitrate", {}) 60 | if not bitrate: 61 | continue 62 | song.song_url = bitrate.get("file_link", "") 63 | if not song.available: # 如果URL拿不到内容 64 | continue 65 | song.duration = bitrate.get("file_duration", 0) 66 | song.rate = bitrate.get("file_bitrate", 128) 67 | song.ext = bitrate.get("file_extension", "mp3") 68 | song.cover_url = res_song_data.get("songinfo", {}).get("pic_radio", "") 69 | songs_list.append(song) 70 | 71 | return songs_list 72 | 73 | 74 | def baidu_playlist(url): 75 | """ Download playlist from baidu music. """ 76 | pass 77 | 78 | 79 | search = baidu_search 80 | playlist = baidu_playlist 81 | -------------------------------------------------------------------------------- /music_dl/addons/xiami.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: xiami.py 6 | @time: 2019-06-13 7 | """ 8 | 9 | import re 10 | import copy 11 | import json 12 | import hashlib 13 | from .. import config 14 | from ..api import MusicApi 15 | from ..song import BasicSong 16 | from ..exceptions import DataError 17 | 18 | __all__ = ["search"] 19 | 20 | 21 | class XiamiApi(MusicApi): 22 | session = copy.deepcopy(MusicApi.session) 23 | session.headers.update({"referer": "http://www.xiami.com/song/play"}) 24 | 25 | @classmethod 26 | def encrypted_params(cls, keyword): 27 | number = config.get("number") or 5 28 | _q = dict(key=keyword, pagingVO=dict(page=1, pageSize=number)) 29 | _q = json.dumps(_q) 30 | url = "https://www.xiami.com/search?key={}".format(keyword) 31 | res = cls.session.get(url) 32 | cookie = res.cookies.get("xm_sg_tk", "").split("_")[0] 33 | origin_str = "%s_xmMain_/api/search/searchSongs_%s" % (cookie, _q) 34 | _s = hashlib.md5(origin_str.encode()).hexdigest() 35 | return dict(_q=_q, _s=_s) 36 | 37 | 38 | class XiamiSong(BasicSong): 39 | def __init__(self): 40 | super(XiamiSong, self).__init__() 41 | 42 | 43 | def xiami_search(keyword) -> list: 44 | """ search music from xiami """ 45 | params = XiamiApi.encrypted_params(keyword=keyword) 46 | print(params) 47 | res_data = ( 48 | XiamiApi.request( 49 | "https://www.xiami.com/api/search/searchSongs", method="GET", data=params 50 | ) 51 | .get("result", {}) 52 | .get("data", {}) 53 | .get("songs", []) 54 | ) 55 | if not res_data: 56 | raise DataError("Get xiami data failed.") 57 | 58 | songs_list = [] 59 | for item in res_data: 60 | song = XiamiSong() 61 | song.source = "xiami" 62 | song.id = item.get("songId", "") 63 | song.title = item.get("songName", "") 64 | song.singer = item.get("singers", "") 65 | song.album = item.get("albumName", "") 66 | song.cover_url = item.get("albumLogo", "") 67 | song.lyrics_url = item.get("lyricInfo", {}).get("lyricFile", "") 68 | 69 | listen_files = sorted( 70 | item.get("listenFiles", []), 71 | key=lambda x: x.get("downloadFileSize", 0), 72 | reverse=True, 73 | ) 74 | song.song_url = listen_files[0].get("listenFile", "") 75 | song.duration = int(listen_files[0].get("length", 0) / 1000) 76 | song.ext = listen_files[0].get("format", "mp3") 77 | song.rate = re.findall("https?://s(\d+)", song.song_url)[0] 78 | 79 | songs_list.append(song) 80 | 81 | return songs_list 82 | 83 | 84 | search = xiami_search 85 | -------------------------------------------------------------------------------- /locale/music-dl.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: music-dl\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-02-19 23:43+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "X-Poedit-Basepath: ..\n" 20 | "X-Poedit-SearchPath-0: .\n" 21 | 22 | #: music_dl/__main__.py:27 23 | #, python-brace-format 24 | msgid "正在搜索 {searchterm} 来自 ..." 25 | msgstr "" 26 | 27 | #: music_dl/__main__.py:45 28 | #, python-brace-format 29 | msgid "音乐列表 {error} 获取失败." 30 | msgstr "" 31 | 32 | #: music_dl/__main__.py:59 33 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 34 | msgstr "" 35 | 36 | #: music_dl/__main__.py:60 37 | msgid "下载序号" 38 | msgstr "" 39 | 40 | #: music_dl/__main__.py:71 41 | msgid "输入有误!" 42 | msgstr "" 43 | 44 | #: music_dl/__main__.py:78 45 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 46 | msgstr "" 47 | 48 | #: music_dl/__main__.py:88 49 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 50 | msgstr "" 51 | 52 | #: music_dl/__main__.py:89 53 | msgid "搜索关键字" 54 | msgstr "" 55 | 56 | #: music_dl/__main__.py:95 57 | msgid "支持的数据源: " 58 | msgstr "" 59 | 60 | #: music_dl/__main__.py:97 61 | msgid "搜索数量限制" 62 | msgstr "" 63 | 64 | #: music_dl/__main__.py:98 65 | msgid "指定输出目录" 66 | msgstr "" 67 | 68 | #: music_dl/__main__.py:99 69 | msgid "指定代理(如http://127.0.0.1:1087)" 70 | msgstr "" 71 | 72 | #: music_dl/__main__.py:100 73 | msgid "对搜索结果去重和排序(默认不去重)" 74 | msgstr "" 75 | 76 | #: music_dl/__main__.py:101 77 | msgid "详细模式" 78 | msgstr "" 79 | 80 | #: music_dl/core.py:46 81 | msgid "下载音乐失败" 82 | msgstr "" 83 | 84 | #: music_dl/music.py:49 85 | #, python-brace-format 86 | msgid "" 87 | " -> 来源: {idx}{source} #{id}\n" 88 | " -> 歌曲: {title}\n" 89 | " -> 歌手: {singer}\n" 90 | " -> 专辑: {album}\n" 91 | " -> 时长: {duration}\n" 92 | " -> 大小: {size}MB\n" 93 | " -> 比特率: {rate}\n" 94 | " -> URL: {url} \n" 95 | msgstr "" 96 | 97 | #: music_dl/music.py:132 98 | #, python-brace-format 99 | msgid "请求失败: {url}" 100 | msgstr "" 101 | 102 | #: music_dl/music.py:173 103 | msgid "下载中..." 104 | msgstr "" 105 | 106 | #: music_dl/music.py:179 107 | #, python-brace-format 108 | msgid "已保存到: {outfile}" 109 | msgstr "" 110 | 111 | #: music_dl/music.py:182 112 | msgid "下载音乐失败: " 113 | msgstr "" 114 | 115 | #: music_dl/music.py:183 116 | #, python-brace-format 117 | msgid "URL: {url}" 118 | msgstr "" 119 | 120 | #: music_dl/music.py:184 121 | #, python-brace-format 122 | msgid "位置: {outfile}" 123 | msgstr "" 124 | -------------------------------------------------------------------------------- /music_dl/addons/migu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: migu 6 | @time: 2019-08-25 7 | """ 8 | 9 | import copy 10 | from .. import config 11 | from ..api import MusicApi 12 | from ..song import BasicSong 13 | 14 | 15 | class MiguApi(MusicApi): 16 | session = copy.deepcopy(MusicApi.session) 17 | session.headers.update( 18 | {"referer": "http://music.migu.cn/", "User-Agent": config.get("ios_useragent")} 19 | ) 20 | 21 | 22 | class MiguSong(BasicSong): 23 | def __init__(self): 24 | super(MiguSong, self).__init__() 25 | self.content_id = "" 26 | 27 | def migu_search(keyword) -> list: 28 | """ 搜索音乐 """ 29 | number = config.get("number") or 5 30 | params = { 31 | "ua": "Android_migu", 32 | "version": "5.0.1", 33 | "text": keyword, 34 | "pageNo": 1, 35 | "pageSize": number, 36 | "searchSwitch": '{"song":1,"album":0,"singer":0,"tagSong":0,"mvSong":0,"songlist":0,"bestShow":1}', 37 | } 38 | 39 | songs_list = [] 40 | MiguApi.session.headers.update( 41 | {"referer": "http://music.migu.cn/", "User-Agent": config.get("ios_useragent")} 42 | ) 43 | res_data = ( 44 | MiguApi.request( 45 | "http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do", 46 | method="GET", 47 | data=params, 48 | ) 49 | .get("songResultData", {}) 50 | .get("result", []) 51 | ) 52 | 53 | for item in res_data: 54 | # 获得歌手名字 55 | singers = [s.get("name", "") for s in item.get("singers", [])] 56 | song = MiguSong() 57 | song.source = "MIGU" 58 | song.id = item.get("id", "") 59 | song.title = item.get("name", "") 60 | song.singer = "、".join(singers) 61 | song.album = item.get("albums", [])[0].get("name", "") 62 | song.cover_url = item.get("imgItems", [])[0].get("img", "") 63 | song.lyrics_url = item.get("lyricUrl", item.get("trcUrl", "")) 64 | # song.duration = item.get("interval", 0) 65 | # 特有字段 66 | song.content_id = item.get("contentId", "") 67 | # 品质从高到低排序 68 | rate_list = sorted( 69 | item.get("rateFormats", []), key=lambda x: int(x["size"]), reverse=True 70 | ) 71 | for rate in rate_list: 72 | url = "http://app.pd.nf.migu.cn/MIGUM2.0/v1.0/content/sub/listenSong.do?toneFlag={formatType}&netType=00&userId=15548614588710179085069&ua=Android_migu&version=5.1©rightId=0&contentId={contentId}&resourceType={resourceType}&channel=0".format( 73 | formatType=rate.get("formatType", "SQ"), 74 | contentId=song.content_id, 75 | resourceType=rate.get("resourceType", "E"), 76 | ) 77 | song.song_url = url 78 | if song.available: 79 | song.size = round(int(rate.get("size", 0)) / 1048576, 2) 80 | ext = "flac" if rate.get("formatType", "") == "SQ" else "mp3" 81 | song.ext = rate.get("fileType", ext) 82 | break 83 | 84 | songs_list.append(song) 85 | 86 | return songs_list 87 | 88 | 89 | search = migu_search 90 | -------------------------------------------------------------------------------- /locale/de/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:43+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:46+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: music_dl/__main__.py:27 22 | #, python-brace-format 23 | msgid "正在搜索 {searchterm} 来自 ..." 24 | msgstr "" 25 | 26 | #: music_dl/__main__.py:45 27 | #, python-brace-format 28 | msgid "音乐列表 {error} 获取失败." 29 | msgstr "" 30 | 31 | #: music_dl/__main__.py:59 32 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 33 | msgstr "" 34 | 35 | #: music_dl/__main__.py:60 36 | msgid "下载序号" 37 | msgstr "" 38 | 39 | #: music_dl/__main__.py:71 40 | msgid "输入有误!" 41 | msgstr "" 42 | 43 | #: music_dl/__main__.py:78 44 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 45 | msgstr "" 46 | 47 | #: music_dl/__main__.py:88 48 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 49 | msgstr "" 50 | 51 | #: music_dl/__main__.py:89 52 | msgid "搜索关键字" 53 | msgstr "" 54 | 55 | #: music_dl/__main__.py:95 56 | msgid "支持的数据源: " 57 | msgstr "" 58 | 59 | #: music_dl/__main__.py:97 60 | msgid "搜索数量限制" 61 | msgstr "" 62 | 63 | #: music_dl/__main__.py:98 64 | msgid "指定输出目录" 65 | msgstr "" 66 | 67 | #: music_dl/__main__.py:99 68 | msgid "指定代理(如http://127.0.0.1:1087)" 69 | msgstr "" 70 | 71 | #: music_dl/__main__.py:100 72 | msgid "对搜索结果去重和排序(默认不去重)" 73 | msgstr "" 74 | 75 | #: music_dl/__main__.py:101 76 | msgid "详细模式" 77 | msgstr "" 78 | 79 | #: music_dl/core.py:46 80 | msgid "下载音乐失败" 81 | msgstr "Track-Download fehlgeschlagen" 82 | 83 | #: music_dl/music.py:49 84 | #, python-brace-format 85 | msgid "" 86 | " -> 来源: {idx}{source} #{id}\n" 87 | " -> 歌曲: {title}\n" 88 | " -> 歌手: {singer}\n" 89 | " -> 专辑: {album}\n" 90 | " -> 时长: {duration}\n" 91 | " -> 大小: {size}MB\n" 92 | " -> 比特率: {rate}\n" 93 | " -> URL: {url} \n" 94 | msgstr "" 95 | " -> Quelle: {idx}{source} #{id}\n" 96 | " -> Titel: {title}\n" 97 | " -> Künstler: {singer}\n" 98 | " -> Album: {album}\n" 99 | " -> Länge: {duration}\n" 100 | " -> Größe: {size}MB\n" 101 | " -> Bewertung: {rate}\n" 102 | " -> URL: {url} \n" 103 | 104 | #: music_dl/music.py:132 105 | #, python-brace-format 106 | msgid "请求失败: {url}" 107 | msgstr "" 108 | 109 | #: music_dl/music.py:173 110 | msgid "下载中..." 111 | msgstr "Herunterladen..." 112 | 113 | #: music_dl/music.py:179 114 | #, python-brace-format 115 | msgid "已保存到: {outfile}" 116 | msgstr "Gespeichert in: {outfile}" 117 | 118 | #: music_dl/music.py:182 119 | msgid "下载音乐失败: " 120 | msgstr "Track-Download fehlgeschlagen: " 121 | 122 | #: music_dl/music.py:183 123 | #, python-brace-format 124 | msgid "URL: {url}" 125 | msgstr "URL: {url}" 126 | 127 | #: music_dl/music.py:184 128 | #, python-brace-format 129 | msgid "位置: {outfile}" 130 | msgstr "Speicherort: {outfile}" 131 | 132 | #~ msgid "Request failed: {url}" 133 | #~ msgstr "Request fehlgeschlagen: {url}" 134 | -------------------------------------------------------------------------------- /locale/jp/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:29+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:31+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: ja\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: music_dl/__main__.py:27 22 | #, python-brace-format 23 | msgid "正在搜索 {searchterm} 来自 ..." 24 | msgstr "" 25 | 26 | #: music_dl/__main__.py:45 27 | #, python-brace-format 28 | msgid "音乐列表 {error} 获取失败." 29 | msgstr "" 30 | 31 | #: music_dl/__main__.py:59 32 | msgid "" 33 | "请输入{下载序号},支持形如 {numbers} 的格式,输" 34 | "入 {N} 跳过下载" 35 | msgstr "" 36 | 37 | #: music_dl/__main__.py:60 38 | msgid "下载序号" 39 | msgstr "ファイルID番号" 40 | 41 | #: music_dl/__main__.py:71 42 | msgid "输入有误!" 43 | msgstr "入力違った!" 44 | 45 | #: music_dl/__main__.py:78 46 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 47 | msgstr "" 48 | "検索したい曲を入力するか、CTRL+Cを押して終了し" 49 | "てください" 50 | 51 | #: music_dl/__main__.py:88 52 | msgid "" 53 | "请输入要搜索的歌曲,名称和歌手一起输入可以提高" 54 | "匹配(如 空帆船 朴树)" 55 | msgstr "" 56 | "検索したい曲を入力してください。 音楽人やアルバ" 57 | "ムなどの検索用語を追加すると、検索結果が向上す" 58 | "ることがあります。 (例えば: Despacito, Luis " 59 | "Fonsi)" 60 | 61 | #: music_dl/__main__.py:89 62 | msgid "搜索关键字" 63 | msgstr "" 64 | 65 | #: music_dl/__main__.py:95 66 | msgid "支持的数据源: " 67 | msgstr "" 68 | 69 | #: music_dl/__main__.py:97 70 | msgid "搜索数量限制" 71 | msgstr "" 72 | 73 | #: music_dl/__main__.py:98 74 | msgid "指定输出目录" 75 | msgstr "" 76 | 77 | #: music_dl/__main__.py:99 78 | msgid "指定代理(如http://127.0.0.1:1087)" 79 | msgstr "" 80 | 81 | #: music_dl/__main__.py:100 82 | msgid "对搜索结果去重和排序(默认不去重)" 83 | msgstr "" 84 | 85 | #: music_dl/__main__.py:101 86 | msgid "详细模式" 87 | msgstr "" 88 | 89 | #: music_dl/core.py:46 90 | msgid "下载音乐失败" 91 | msgstr "音楽ダウンロードが失敗した" 92 | 93 | #: music_dl/music.py:49 94 | #, python-brace-format 95 | msgid "" 96 | " -> 来源: {idx}{source} #{id}\n" 97 | " -> 歌曲: {title}\n" 98 | " -> 歌手: {singer}\n" 99 | " -> 专辑: {album}\n" 100 | " -> 时长: {duration}\n" 101 | " -> 大小: {size}MB\n" 102 | " -> 比特率: {rate}\n" 103 | " -> URL: {url} \n" 104 | msgstr "" 105 | " -> ソース;{idx}{source} #{id}\n" 106 | " -> タイトル;{title}\n" 107 | " -> 音楽人;{singer}\n" 108 | " -> アルバム;{album}\n" 109 | " -> 音楽の長さ; {duration}\n" 110 | " -> ファイルサイズ; {size}MB\n" 111 | " -> 格付け: {rate}\n" 112 | " -> URL: {url} \n" 113 | 114 | #: music_dl/music.py:132 115 | #, python-brace-format 116 | msgid "Request failed: {url}" 117 | msgstr "リクエストが失敗した: {url}" 118 | 119 | #: music_dl/music.py:173 120 | msgid "下载中..." 121 | msgstr "ダウンロード中。。。" 122 | 123 | #: music_dl/music.py:179 124 | #, python-brace-format 125 | msgid "已保存到: {outfile}" 126 | msgstr "セーブしたに: {outfile}" 127 | 128 | #: music_dl/music.py:182 129 | msgid "下载音乐失败: " 130 | msgstr "音楽ダウンロードが失敗した: " 131 | 132 | #: music_dl/music.py:183 133 | #, python-brace-format 134 | msgid "URL: {url}" 135 | msgstr "URL: {url}" 136 | 137 | #: music_dl/music.py:184 138 | #, python-brace-format 139 | msgid "位置: {outfile}" 140 | msgstr "ファイル場所: {outfile}" 141 | -------------------------------------------------------------------------------- /locale/zh_CN/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:47+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:47+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: zh\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | "X-Poedit-Basepath: ../../..\n" 21 | "X-Poedit-SearchPath-0: .\n" 22 | 23 | #: music_dl/__main__.py:27 24 | #, python-brace-format 25 | msgid "正在搜索 {searchterm} 来自 ..." 26 | msgstr "正在搜索 {searchterm} 来自 ..." 27 | 28 | #: music_dl/__main__.py:45 29 | #, python-brace-format 30 | msgid "音乐列表 {error} 获取失败." 31 | msgstr "音乐列表 {error} 获取失败." 32 | 33 | #: music_dl/__main__.py:59 34 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 35 | msgstr "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 36 | 37 | #: music_dl/__main__.py:60 38 | msgid "下载序号" 39 | msgstr "下载序号" 40 | 41 | #: music_dl/__main__.py:71 42 | msgid "输入有误!" 43 | msgstr "输入有误!" 44 | 45 | #: music_dl/__main__.py:78 46 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 47 | msgstr "请输入要搜索的歌曲,或Ctrl+C退出" 48 | 49 | #: music_dl/__main__.py:88 50 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 51 | msgstr "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 52 | 53 | #: music_dl/__main__.py:89 54 | msgid "搜索关键字" 55 | msgstr "搜索关键字" 56 | 57 | #: music_dl/__main__.py:95 58 | msgid "支持的数据源: " 59 | msgstr "支持的数据源: " 60 | 61 | #: music_dl/__main__.py:97 62 | msgid "搜索数量限制" 63 | msgstr "搜索数量限制" 64 | 65 | #: music_dl/__main__.py:98 66 | msgid "指定输出目录" 67 | msgstr "指定输出目录" 68 | 69 | #: music_dl/__main__.py:99 70 | msgid "指定代理(如http://127.0.0.1:1087)" 71 | msgstr "指定代理(如http://127.0.0.1:1087)" 72 | 73 | #: music_dl/__main__.py:100 74 | msgid "对搜索结果去重和排序(默认不去重)" 75 | msgstr "对搜索结果去重和排序(默认不去重)" 76 | 77 | #: music_dl/__main__.py:101 78 | msgid "详细模式" 79 | msgstr "详细模式" 80 | 81 | #: music_dl/core.py:46 82 | msgid "下载音乐失败" 83 | msgstr "下载音乐失败" 84 | 85 | #: music_dl/music.py:49 86 | #, python-brace-format 87 | msgid "" 88 | " -> 来源: {idx}{source} #{id}\n" 89 | " -> 歌曲: {title}\n" 90 | " -> 歌手: {singer}\n" 91 | " -> 专辑: {album}\n" 92 | " -> 时长: {duration}\n" 93 | " -> 大小: {size}MB\n" 94 | " -> 比特率: {rate}\n" 95 | " -> URL: {url} \n" 96 | msgstr "" 97 | " -> 来源: {idx}{source} #{id}\n" 98 | " -> 歌曲: {title}\n" 99 | " -> 歌手: {singer}\n" 100 | " -> 专辑: {album}\n" 101 | " -> 时长: {duration}\n" 102 | " -> 大小: {size}MB\n" 103 | " -> 比特率: {rate}\n" 104 | " -> URL: {url} \n" 105 | 106 | #: music_dl/music.py:132 107 | #, python-brace-format 108 | msgid "请求失败: {url}" 109 | msgstr "请求失败: {url}" 110 | 111 | #: music_dl/music.py:173 112 | msgid "下载中..." 113 | msgstr "下载中..." 114 | 115 | #: music_dl/music.py:179 116 | #, python-brace-format 117 | msgid "已保存到: {outfile}" 118 | msgstr "已保存到: {outfile}" 119 | 120 | #: music_dl/music.py:182 121 | msgid "下载音乐失败: " 122 | msgstr "下载音乐失败: " 123 | 124 | #: music_dl/music.py:183 125 | #, python-brace-format 126 | msgid "URL: {url}" 127 | msgstr "URL: {url}" 128 | 129 | #: music_dl/music.py:184 130 | #, python-brace-format 131 | msgid "位置: {outfile}" 132 | msgstr "位置: {outfile}" 133 | 134 | #~ msgid "Request failed: {url}" 135 | #~ msgstr "Request failed: {url}" 136 | -------------------------------------------------------------------------------- /locale/hr/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:43+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:46+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: hr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" 21 | 22 | #: music_dl/__main__.py:27 23 | #, python-brace-format 24 | msgid "正在搜索 {searchterm} 来自 ..." 25 | msgstr "" 26 | 27 | #: music_dl/__main__.py:45 28 | #, python-brace-format 29 | msgid "音乐列表 {error} 获取失败." 30 | msgstr "" 31 | 32 | #: music_dl/__main__.py:59 33 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 34 | msgstr "" 35 | "Molim vas unesite [下载序号] datoteke koje želite da skinete ili pretisnite " 36 | "N da odkažete proces" 37 | 38 | #: music_dl/__main__.py:60 39 | msgid "下载序号" 40 | msgstr "ID Datoteke" 41 | 42 | #: music_dl/__main__.py:71 43 | msgid "输入有误!" 44 | msgstr "Pogrešan podatak data!" 45 | 46 | #: music_dl/__main__.py:78 47 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 48 | msgstr "Molim vas da unesete pesmu koju želite li pritisnite CTRL+C da izađete" 49 | 50 | #: music_dl/__main__.py:88 51 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 52 | msgstr "" 53 | "Molim vas da unesete pesmu koju želite, Dodajte dodatne podatke kao što su " 54 | "Umetnik i Album, može poboljšati rezultate potrage. (Na primer: Despacito, " 55 | "Luis Fonsi)" 56 | 57 | #: music_dl/__main__.py:89 58 | msgid "搜索关键字" 59 | msgstr "" 60 | 61 | #: music_dl/__main__.py:95 62 | msgid "支持的数据源: " 63 | msgstr "" 64 | 65 | #: music_dl/__main__.py:97 66 | msgid "搜索数量限制" 67 | msgstr "" 68 | 69 | #: music_dl/__main__.py:98 70 | msgid "指定输出目录" 71 | msgstr "" 72 | 73 | #: music_dl/__main__.py:99 74 | msgid "指定代理(如http://127.0.0.1:1087)" 75 | msgstr "" 76 | 77 | #: music_dl/__main__.py:100 78 | msgid "对搜索结果去重和排序(默认不去重)" 79 | msgstr "" 80 | 81 | #: music_dl/__main__.py:101 82 | msgid "详细模式" 83 | msgstr "" 84 | 85 | #: music_dl/core.py:46 86 | msgid "下载音乐失败" 87 | msgstr "Traka neuspešno skinuta" 88 | 89 | #: music_dl/music.py:49 90 | #, python-brace-format 91 | msgid "" 92 | " -> 来源: {idx}{source} #{id}\n" 93 | " -> 歌曲: {title}\n" 94 | " -> 歌手: {singer}\n" 95 | " -> 专辑: {album}\n" 96 | " -> 时长: {duration}\n" 97 | " -> 大小: {size}MB\n" 98 | " -> 比特率: {rate}\n" 99 | " -> URL: {url} \n" 100 | msgstr "" 101 | " -> Izvor: {idx}{source} #{id}\n" 102 | " -> Naslov: {title}\n" 103 | " -> Umetnik: {singer}\n" 104 | " -> Album: {album}\n" 105 | " -> Dužina: {duration}\n" 106 | " -> Veličina Datoteke: {size}MB\n" 107 | " -> Ocjena: {rate}\n" 108 | " -> URL: {url} \n" 109 | 110 | #: music_dl/music.py:132 111 | #, python-brace-format 112 | msgid "请求失败: {url}" 113 | msgstr "" 114 | 115 | #: music_dl/music.py:173 116 | msgid "下载中..." 117 | msgstr "Skidam..." 118 | 119 | #: music_dl/music.py:179 120 | #, python-brace-format 121 | msgid "已保存到: {outfile}" 122 | msgstr "Sačuvaj kao: {outfile}" 123 | 124 | #: music_dl/music.py:182 125 | msgid "下载音乐失败: " 126 | msgstr "Traka neuspešno skinuta: " 127 | 128 | #: music_dl/music.py:183 129 | #, python-brace-format 130 | msgid "URL: {url}" 131 | msgstr "URL: {url}" 132 | 133 | #: music_dl/music.py:184 134 | #, python-brace-format 135 | msgid "位置: {outfile}" 136 | msgstr "Lokacija datoteke: {outfile}" 137 | 138 | #~ msgid "Request failed: {url}" 139 | #~ msgstr "Željba neuspešna: {url}" 140 | -------------------------------------------------------------------------------- /locale/sr/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:43+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:47+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: sr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" 21 | 22 | #: music_dl/__main__.py:27 23 | #, python-brace-format 24 | msgid "正在搜索 {searchterm} 来自 ..." 25 | msgstr "" 26 | 27 | #: music_dl/__main__.py:45 28 | #, python-brace-format 29 | msgid "音乐列表 {error} 获取失败." 30 | msgstr "" 31 | 32 | #: music_dl/__main__.py:59 33 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 34 | msgstr "" 35 | "Молим вас унесите [ИД број] датотеке које желите да скинете или претисните Н " 36 | "да одкажете процес" 37 | 38 | #: music_dl/__main__.py:60 39 | msgid "下载序号" 40 | msgstr "ИД Датотеке" 41 | 42 | #: music_dl/__main__.py:71 43 | msgid "输入有误!" 44 | msgstr "Погрешан податак дат!" 45 | 46 | #: music_dl/__main__.py:78 47 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 48 | msgstr "" 49 | "Молим вас да унесете песму коју желите или притисните CTRL+C да изађете" 50 | 51 | #: music_dl/__main__.py:88 52 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 53 | msgstr "" 54 | "Молим вас да унесете песму коју желите. Додајте додатне податке као што су " 55 | "Уметник и Албум, може поболјшати резултате потраге. (На пример: Despacito, " 56 | "Luis Fonsi)" 57 | 58 | #: music_dl/__main__.py:89 59 | msgid "搜索关键字" 60 | msgstr "" 61 | 62 | #: music_dl/__main__.py:95 63 | msgid "支持的数据源: " 64 | msgstr "" 65 | 66 | #: music_dl/__main__.py:97 67 | msgid "搜索数量限制" 68 | msgstr "" 69 | 70 | #: music_dl/__main__.py:98 71 | msgid "指定输出目录" 72 | msgstr "" 73 | 74 | #: music_dl/__main__.py:99 75 | msgid "指定代理(如http://127.0.0.1:1087)" 76 | msgstr "" 77 | 78 | #: music_dl/__main__.py:100 79 | msgid "对搜索结果去重和排序(默认不去重)" 80 | msgstr "" 81 | 82 | #: music_dl/__main__.py:101 83 | msgid "详细模式" 84 | msgstr "" 85 | 86 | #: music_dl/core.py:46 87 | msgid "下载音乐失败" 88 | msgstr "Трака неуспешно скинута" 89 | 90 | #: music_dl/music.py:49 91 | #, python-brace-format 92 | msgid "" 93 | " -> 来源: {idx}{source} #{id}\n" 94 | " -> 歌曲: {title}\n" 95 | " -> 歌手: {singer}\n" 96 | " -> 专辑: {album}\n" 97 | " -> 时长: {duration}\n" 98 | " -> 大小: {size}MB\n" 99 | " -> 比特率: {rate}\n" 100 | " -> URL: {url} \n" 101 | msgstr "" 102 | " -> Извор: {idx}{source} #{id}\n" 103 | " -> Наслов: {title}\n" 104 | " -> Уметникt: {singer}\n" 105 | " -> Албум: {album}\n" 106 | " -> Дужина: {duration}\n" 107 | " -> Величина Датотеке: {size}МБ\n" 108 | " -> Ратинг: {rate}\n" 109 | " -> УРЛ: {url}\n" 110 | 111 | #: music_dl/music.py:132 112 | #, python-brace-format 113 | msgid "请求失败: {url}" 114 | msgstr "" 115 | 116 | #: music_dl/music.py:173 117 | msgid "下载中..." 118 | msgstr "Скидам..." 119 | 120 | #: music_dl/music.py:179 121 | #, python-brace-format 122 | msgid "已保存到: {outfile}" 123 | msgstr "Сачувај Као: {outfile}" 124 | 125 | #: music_dl/music.py:182 126 | msgid "下载音乐失败: " 127 | msgstr "Трака неуспешно скинута: " 128 | 129 | #: music_dl/music.py:183 130 | #, python-brace-format 131 | msgid "URL: {url}" 132 | msgstr "УРЛ: {url}" 133 | 134 | #: music_dl/music.py:184 135 | #, python-brace-format 136 | msgid "位置: {outfile}" 137 | msgstr "Локација Датотеке: {outfile}" 138 | 139 | #~ msgid "Request failed: {url}" 140 | #~ msgstr "Желјба неуспешна: {url}" 141 | -------------------------------------------------------------------------------- /locale/en/LC_MESSAGES/music-dl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-02-19 23:43+0800\n" 11 | "PO-Revision-Date: 2019-02-19 23:44+0800\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: en\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.2.1\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: music_dl/__main__.py:27 22 | #, python-brace-format 23 | msgid "正在搜索 {searchterm} 来自 ..." 24 | msgstr "Searching {searchterm} from …" 25 | 26 | #: music_dl/__main__.py:45 27 | #, python-brace-format 28 | msgid "音乐列表 {error} 获取失败." 29 | msgstr "Music list {error} obtain failed." 30 | 31 | #: music_dl/__main__.py:59 32 | msgid "请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载" 33 | msgstr "" 34 | "Please enter some {下载序号} representing the songs you want,you can " 35 | "also use a range, for example {numbers},or you could enter {N} to not " 36 | "download any songs" 37 | 38 | #: music_dl/__main__.py:60 39 | msgid "下载序号" 40 | msgstr "numbers" 41 | 42 | #: music_dl/__main__.py:71 43 | msgid "输入有误!" 44 | msgstr "Incorrect input given!" 45 | 46 | #: music_dl/__main__.py:78 47 | msgid "请输入要搜索的歌曲,或Ctrl+C退出" 48 | msgstr "" 49 | "Please enter what song you'd like to search for, or press CTRL+C to exit" 50 | 51 | #: music_dl/__main__.py:88 52 | msgid "请输入要搜索的歌曲,名称和歌手一起输入可以提高匹配(如 空帆船 朴树)" 53 | msgstr "" 54 | "Please enter the song you want to search. Adding extra search terms like " 55 | "Artist and Album can improve results. (e.g. Despacito, Luis Fonsi)" 56 | 57 | #: music_dl/__main__.py:89 58 | msgid "搜索关键字" 59 | msgstr "Keyword" 60 | 61 | #: music_dl/__main__.py:95 62 | msgid "支持的数据源: " 63 | msgstr "Supported music source: " 64 | 65 | #: music_dl/__main__.py:97 66 | msgid "搜索数量限制" 67 | msgstr "Number of search results" 68 | 69 | #: music_dl/__main__.py:98 70 | msgid "指定输出目录" 71 | msgstr "Output directory" 72 | 73 | #: music_dl/__main__.py:99 74 | msgid "指定代理(如http://127.0.0.1:1087)" 75 | msgstr "Proxy (e.g. http://127.0.0.1:1087)" 76 | 77 | #: music_dl/__main__.py:100 78 | msgid "对搜索结果去重和排序(默认不去重)" 79 | msgstr "Merge search results (Default does not merge)" 80 | 81 | #: music_dl/__main__.py:101 82 | msgid "详细模式" 83 | msgstr "Verbose mode" 84 | 85 | #: music_dl/core.py:46 86 | msgid "下载音乐失败" 87 | msgstr "Track download failed" 88 | 89 | #: music_dl/music.py:49 90 | #, python-brace-format 91 | msgid "" 92 | " -> 来源: {idx}{source} #{id}\n" 93 | " -> 歌曲: {title}\n" 94 | " -> 歌手: {singer}\n" 95 | " -> 专辑: {album}\n" 96 | " -> 时长: {duration}\n" 97 | " -> 大小: {size}MB\n" 98 | " -> 比特率: {rate}\n" 99 | " -> URL: {url} \n" 100 | msgstr "" 101 | " -> Source: {idx}{source} #{id}\n" 102 | " -> Title: {title}\n" 103 | " -> Artist: {singer}\n" 104 | " -> Album: {album}\n" 105 | " -> Length: {duration}\n" 106 | " -> Filesize: {size}MB\n" 107 | " -> Bitrate: {rate}\n" 108 | " -> URL: {url} \n" 109 | 110 | #: music_dl/music.py:132 111 | #, python-brace-format 112 | msgid "请求失败: {url}" 113 | msgstr "Request failed: {url}" 114 | 115 | #: music_dl/music.py:173 116 | msgid "下载中..." 117 | msgstr "Downloading..." 118 | 119 | #: music_dl/music.py:179 120 | #, python-brace-format 121 | msgid "已保存到: {outfile}" 122 | msgstr "Saved to: {outfile}" 123 | 124 | #: music_dl/music.py:182 125 | msgid "下载音乐失败: " 126 | msgstr "Track download failed: " 127 | 128 | #: music_dl/music.py:183 129 | #, python-brace-format 130 | msgid "URL: {url}" 131 | msgstr "URL: {url}" 132 | 133 | #: music_dl/music.py:184 134 | #, python-brace-format 135 | msgid "位置: {outfile}" 136 | msgstr "File location: {outfile}" 137 | 138 | #~ msgid "Request failed: {url}" 139 | #~ msgstr "Request failed: {url}" 140 | -------------------------------------------------------------------------------- /music_dl/addons/qq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: qq.py 6 | @time: 2019-05-08 7 | """ 8 | 9 | import random 10 | import base64 11 | import copy 12 | from .. import config 13 | from ..api import MusicApi 14 | from ..song import BasicSong 15 | 16 | 17 | class QQApi(MusicApi): 18 | session = copy.deepcopy(MusicApi.session) 19 | session.headers.update( 20 | { 21 | "referer": "https://y.qq.com/portal/player.html", 22 | "User-Agent": config.get("ios_useragent"), 23 | } 24 | ) 25 | 26 | 27 | class QQSong(BasicSong): 28 | def __init__(self): 29 | super(QQSong, self).__init__() 30 | self.mid = "" 31 | 32 | def download_lyrics(self): 33 | url = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg" 34 | params = { 35 | "songmid": self.mid, 36 | "loginUin": "0", 37 | "hostUin": "0", 38 | "format": "json", 39 | "inCharset": "utf8", 40 | "outCharset": "utf-8", 41 | "notice": "0", 42 | "platform": "yqq.json", 43 | "needNewCode": "0", 44 | } 45 | 46 | res_data = QQApi.request( 47 | "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg", 48 | method="GET", 49 | data=params, 50 | ) 51 | lyric = res_data.get("lyric", "") 52 | self.lyrics_text = base64.b64decode(lyric).decode("utf-8") 53 | super(QQSong, self)._save_lyrics_text() 54 | 55 | def download_cover(self): 56 | pass 57 | 58 | def download(self): 59 | # 计算vkey 60 | guid = str(random.randrange(1000000000, 10000000000)) 61 | params = { 62 | "guid": guid, 63 | "loginUin": "3051522991", 64 | "format": "json", 65 | "platform": "yqq", 66 | "cid": "205361747", 67 | "uin": "3051522991", 68 | "songmid": self.mid, 69 | "needNewCode": 0, 70 | } 71 | rate_list = [ 72 | ("A000", "ape", 800), 73 | ("F000", "flac", 800), 74 | ("M800", "mp3", 320), 75 | ("C400", "m4a", 128), 76 | ("M500", "mp3", 128), 77 | ] 78 | QQApi.session.headers.update({"referer": "http://y.qq.com"}) 79 | for rate in rate_list: 80 | params["filename"] = "%s%s.%s" % (rate[0], self.mid, rate[1]) 81 | res_data = QQApi.request( 82 | "https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg", 83 | method="GET", 84 | data=params, 85 | ) 86 | vkey = res_data.get("data", {}).get("items", [])[0].get("vkey", "") 87 | if vkey: 88 | url = ( 89 | "http://dl.stream.qqmusic.qq.com/%s?vkey=%s&guid=%s&uin=3051522991&fromtag=64" 90 | % (params["filename"], vkey, guid) 91 | ) 92 | self.song_url = url 93 | if self.available: 94 | self.ext = rate[1] 95 | self.rate = rate[2] 96 | break 97 | super(QQSong, self).download() 98 | 99 | 100 | def qq_search(keyword) -> list: 101 | """ 搜索音乐 """ 102 | number = config.get("number") or 5 103 | params = {"w": keyword, "format": "json", "p": 1, "n": number} 104 | 105 | songs_list = [] 106 | QQApi.session.headers.update( 107 | {"referer": "http://m.y.qq.com", "User-Agent": config.get("ios_useragent")} 108 | ) 109 | res_data = ( 110 | QQApi.request( 111 | "http://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp", 112 | method="GET", 113 | data=params, 114 | ) 115 | .get("data", {}) 116 | .get("song", {}) 117 | .get("list", []) 118 | ) 119 | 120 | for item in res_data: 121 | # 获得歌手名字 122 | singers = [s.get("name", "") for s in item.get("singer", "")] 123 | song = QQSong() 124 | song.source = "qq" 125 | song.id = item.get("songid", "") 126 | song.title = item.get("songname", "") 127 | song.singer = "、".join(singers) 128 | song.album = item.get("albumname", "") 129 | song.duration = item.get("interval", 0) 130 | song.size = round(item.get("size128", 0) / 1048576, 2) 131 | # 特有字段 132 | song.mid = item.get("songmid", "") 133 | 134 | songs_list.append(song) 135 | 136 | return songs_list 137 | 138 | 139 | def qq_playlist(url): 140 | pass 141 | 142 | 143 | search = qq_search 144 | playlist = qq_playlist 145 | -------------------------------------------------------------------------------- /music_dl/addons/kugou.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: kugou.py 6 | @time: 2019-05-08 7 | """ 8 | 9 | import copy 10 | from .. import config 11 | from ..api import MusicApi 12 | from ..song import BasicSong 13 | from urllib.parse import urlparse, parse_qs 14 | import math 15 | 16 | class KugouApi(MusicApi): 17 | session = copy.deepcopy(MusicApi.session) 18 | session.headers.update( 19 | {"referer": "http://m.kugou.com", "User-Agent": config.get("ios_headers")} 20 | ) 21 | 22 | 23 | class KugouSong(BasicSong): 24 | def __init__(self): 25 | super(KugouSong, self).__init__() 26 | self.hash = "" 27 | 28 | def download_lyrics(self): 29 | url = f"http://krcs.kugou.com/search?ver=1&client=mobi&duration=&hash={self.hash}&album_audio_id=" 30 | req = KugouApi.request(url, method="GET") 31 | id = req.get('candidates')[0].get('id') 32 | accesskey = req.get('candidates')[0].get('accesskey') 33 | song = req.get('candidates')[0].get('song') 34 | url_lrc = f"http://lyrics.kugou.com/download?ver=1&client=pc&id={id}&accesskey={accesskey}&fmt=lrc&charset=utf8" 35 | res_lrc = KugouApi.request( 36 | url_lrc, method="GET" 37 | ) 38 | import base64 39 | self.lyrics_text = base64.b64decode(res_lrc.get('content')).decode("utf-8") 40 | if self.lyrics_text: 41 | super(KugouSong, self)._save_lyrics_text() 42 | 43 | def download(self): 44 | params = dict(cmd="playInfo", hash=self.hash) 45 | res_data = KugouApi.request( 46 | "http://m.kugou.com/app/i/getSongInfo.php", method="GET", data=params 47 | ) 48 | if not res_data.get("url", ""): 49 | self.logger.error(self.name + " @KUGOU is not available.") 50 | return 51 | self.song_url = res_data.get("url", "") 52 | self.rate = res_data.get("bitRate", 128) 53 | self.ext = res_data.get("extName", "mp3") 54 | self.cover_url = res_data.get("album_img", "").replace("{size}", "150") 55 | 56 | super(KugouSong, self).download() 57 | 58 | 59 | def kugou_search(keyword) -> list: 60 | """搜索音乐""" 61 | number = config.get("number") or 5 62 | params = dict( 63 | keyword=keyword, platform="WebFilter", format="json", page=1, pagesize=number 64 | ) 65 | 66 | songs_list = [] 67 | res_data = ( 68 | KugouApi.request( 69 | "http://songsearch.kugou.com/song_search_v2", method="GET", data=params 70 | ) 71 | .get("data", {}) 72 | .get("lists", []) 73 | ) 74 | 75 | for item in res_data: 76 | song = KugouSong() 77 | song.source = "kugou" 78 | song.id = item.get("Scid", "") 79 | song.title = item.get("SongName", "") 80 | song.singer = item.get("SingerName", "") 81 | song.duration = item.get("Duration", 0) 82 | song.album = item.get("AlbumName", "") 83 | song.size = round(item.get("FileSize", 0) / 1048576, 2) 84 | song.hash = item.get("FileHash", "") 85 | # 如果有更高品质的音乐选择高品质(尽管好像没什么卵用) 86 | keys_list = ["SQFileHash", "HQFileHash"] 87 | for key in keys_list: 88 | hash = item.get(key, "") 89 | if hash and hash != "00000000000000000000000000000000": 90 | song.hash = hash 91 | break 92 | songs_list.append(song) 93 | 94 | return songs_list 95 | 96 | def repeat_get_resource(query) -> list: 97 | return KugouApi.request("https://m3ws.kugou.com/zlist/list", method="GET", data=query).get('list', {}).get('info', []) 98 | 99 | def kugou_playlist(url) -> list: 100 | songs_list = [] 101 | res = KugouApi.requestInstance( 102 | url, 103 | method="GET", 104 | ) 105 | url = urlparse(res.url) 106 | query = parse_qs(url.query) 107 | query["page"] = 1 108 | query["pagesize"] = 100 # 最大100 109 | res_list = ( 110 | KugouApi.request("https://m3ws.kugou.com/zlist/list", method="GET", data=query).get('list', {}) 111 | ) 112 | res_data = res_list.get('info', []) 113 | res_count = res_list.get('count', 0) 114 | repeat_count = math.floor(res_count / (query["page"] * query["pagesize"])) 115 | 116 | while repeat_count > 0: 117 | repeat_count -= 1 118 | query["page"] += 1 119 | for item in repeat_get_resource(query): 120 | res_data.append(item) 121 | 122 | for item in res_data: 123 | song = KugouSong() 124 | song.source = "kugou" 125 | song.id = item.get("fileid", "") 126 | singer_title = item.get("name", "").split(' - ') 127 | song.title = singer_title[1] 128 | song.singer = singer_title[0] 129 | song.duration = int(item.get("timelen", 0) / 1000) 130 | song.album = item.get("album_id", "") 131 | song.size = round(item.get("size", 0) / 1048576, 2) 132 | song.hash = item.get("hash", "") 133 | songs_list.append(song) 134 | 135 | return songs_list 136 | 137 | 138 | search = kugou_search 139 | playlist = kugou_playlist 140 | -------------------------------------------------------------------------------- /music_dl/source.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: source.py 6 | @time: 2019-05-13 7 | """ 8 | 9 | """ 10 | Music source proxy object 11 | """ 12 | 13 | import re 14 | import threading 15 | import importlib 16 | import traceback 17 | import logging 18 | import click 19 | from . import config 20 | from .utils import colorize 21 | from .exceptions import * 22 | 23 | 24 | class MusicSource: 25 | """ 26 | Music source proxy object 27 | """ 28 | 29 | def __init__(self): 30 | self.logger = logging.getLogger(__name__) 31 | 32 | def search(self, keyword, sources_list) -> list: 33 | sources_map = { 34 | "baidu": "baidu", 35 | # "flac": "flac", 36 | "kugou": "kugou", 37 | "netease": "netease", 38 | "163": "netease", 39 | "qq": "qq", 40 | "migu": "migu", 41 | # "xiami": "xiami", 42 | } 43 | thread_pool = [] 44 | ret_songs_list = [] 45 | ret_errors = [] 46 | 47 | click.echo("") 48 | click.echo( 49 | _("Searching {keyword} from ...").format( 50 | keyword=colorize(config.get("keyword"), "highlight") 51 | ), 52 | nl=False, 53 | ) 54 | 55 | for source_key in sources_list: 56 | if not source_key in sources_map: 57 | raise ParameterError("Invalid music source.") 58 | 59 | t = threading.Thread( 60 | target=self.search_thread, 61 | args=(sources_map.get(source_key), keyword, ret_songs_list, ret_errors), 62 | ) 63 | thread_pool.append(t) 64 | t.start() 65 | 66 | for t in thread_pool: 67 | t.join() 68 | 69 | click.echo("") 70 | # 输出错误信息 71 | for err in ret_errors: 72 | self.logger.debug(_("音乐列表 {error} 获取失败.").format(error=err[0].upper())) 73 | self.logger.debug(err[1]) 74 | 75 | # 对搜索结果排序和去重 76 | if not config.get("nomerge"): 77 | ret_songs_list.sort( 78 | key=lambda song: (song.singer, song.title, song.size), reverse=True 79 | ) 80 | tmp_list = [] 81 | for i in range(len(ret_songs_list)): 82 | # 如果名称、歌手都一致的话就去重,保留最大的文件 83 | if ( 84 | i > 0 85 | and ret_songs_list[i].size <= ret_songs_list[i - 1].size 86 | and ret_songs_list[i].title == ret_songs_list[i - 1].title 87 | and ret_songs_list[i].singer == ret_songs_list[i - 1].singer 88 | ): 89 | continue 90 | tmp_list.append(ret_songs_list[i]) 91 | ret_songs_list = tmp_list 92 | 93 | return ret_songs_list 94 | 95 | def search_thread(self, source, keyword, ret_songs_list, ret_errors): 96 | try: 97 | addon = importlib.import_module(".addons." + source, __package__) 98 | ret_songs_list += addon.search(keyword) 99 | except (RequestError, ResponseError, DataError) as e: 100 | ret_errors.append((source, e)) 101 | except Exception as e: 102 | # 最后一起输出错误信息免得影响搜索结果列表排版 103 | err = traceback.format_exc() if config.get("verbose") else str(e) 104 | ret_errors.append((source, err)) 105 | finally: 106 | # 放在搜索后输出是为了营造出搜索很快的假象 107 | click.echo(" %s ..." % colorize(source.upper(), source), nl=False) 108 | 109 | def single(self, url): 110 | sources_map = { 111 | # "baidu.com": "baidu", 112 | # "flac": "flac", 113 | # "kugou.com": "kugou", 114 | "163.com": "netease", 115 | # "qq.com": "qq", 116 | # "xiami.com": "xiami", 117 | } 118 | sources = [v for k, v in sources_map.items() if k in url] 119 | if not sources: 120 | raise ParameterError("Invalid url.") 121 | source = sources[0] 122 | click.echo(_("Downloading song from %s ..." % source.upper())) 123 | try: 124 | addon = importlib.import_module(".addons." + source, __package__) 125 | song = addon.single(url) 126 | return song 127 | except (RequestError, ResponseError, DataError) as e: 128 | self.logger.error(e) 129 | except Exception as e: 130 | # 最后一起输出错误信息免得影响搜索结果列表排版 131 | err = traceback.format_exc() if config.get("verbose") else str(e) 132 | self.logger.error(err) 133 | 134 | def playlist(self, url) -> list: 135 | sources_map = { 136 | # "baidu.com": "baidu", 137 | # "flac": "flac", 138 | "kugou.com": "kugou", 139 | "163.com": "netease", 140 | # "qq.com": "qq", 141 | # "xiami.com": "xiami", 142 | } 143 | sources = [v for k, v in sources_map.items() if k in url] 144 | if not sources: 145 | raise ParameterError("Invalid url.") 146 | source = sources[0] 147 | click.echo(_("Parsing music playlist from %s ..." % source.upper())) 148 | ret_songs_list = [] 149 | try: 150 | addon = importlib.import_module(".addons." + source, __package__) 151 | ret_songs_list = addon.playlist(url) 152 | except (RequestError, ResponseError, DataError) as e: 153 | self.logger.error(e) 154 | except Exception as e: 155 | # 最后一起输出错误信息免得影响搜索结果列表排版 156 | err = traceback.format_exc() if config.get("verbose") else str(e) 157 | self.logger.error(err) 158 | 159 | return ret_songs_list 160 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # Music-dl: Listen to what you want 2 | 3 |

4 | 5 | music-dl 6 | 7 |

8 |
9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | **[Music-dl](https://github.com/0xHJK/music-dl)** is a command line tool which helps you search and download music from multiple sources. 24 | 25 | Support for QQ music, Netease music, Xiami music, Kugou music and Baidu music. See [supported sources](#supported-sources). 26 | 27 | **Python3 Only. Python 3.5+ Recommended.** 28 | 29 | English | [中文文档](https://github.com/0xHJK/music-dl/blob/master/README.md) 30 | 31 | > Note: Some music sources may not be available in some countries and regions. If that happens, you could use Chinese proxies. See for public proxies. 32 | 33 | - Support for lossless music 34 | - Search for high-quality music with priority ( flac -> 320K -> 128K ) 35 | - Support for HTTP and SOCKS proxy 36 | - Support for multithreading searching 37 | - Support for merging and sorting results 38 | - Support keyword highlighting 39 | - Support for filtering search results by file size and song duration (using `--filter` parameter) 40 | - Support for auto-play after downloading (using `--play` parameter) 41 | - Player supports common controls: space to pause/resume, left/right to switch songs, ,/. to move 5 seconds backward/forward, q to quit 42 | - While playing, press d to delete songs you don't like (removes from disk and playlist) 43 | 44 | ## Installation 45 | 46 | Install using pip (Recommended) 47 | 48 | ```bash 49 | $ pip3 install pymusic-dl 50 | ``` 51 | 52 | Manual 53 | 54 | ```bash 55 | $ git clone https://github.com/0xHJK/music-dl.git 56 | $ cd music-dl 57 | $ python3 setup.py install 58 | ``` 59 | 60 | Use directly 61 | 62 | ```bash 63 | $ git clone https://github.com/0xHJK/music-dl.git 64 | $ cd music-dl 65 | $ pip3 install -r requirements.txt 66 | $ ./music-dl 67 | 68 | # OR python3 music-dl 69 | ``` 70 | 71 | ## Usage 72 | 73 | ``` 74 | $ music-dl --help 75 | Usage: music-dl [OPTIONS] 76 | 77 | Search and download music from netease, qq, kugou, baidu and xiami. 78 | Example: music-dl -k "Bruno Mars" 79 | 80 | Options: 81 | --version Show the version and exit. 82 | -k, --keyword TEXT Query keyword 83 | -s, --source TEXT Support for qq netease kugou baidu xiami flac 84 | -c, --count INTEGER Searching count limit (default: 5) 85 | -o, --outdir TEXT Output dir (default: current dir) 86 | -x, --proxy TEXT Set proxy (like http://127.0.0.1:1087) 87 | -m, --merge Sort and merge 88 | -v, --verbose Verbose mode 89 | --lyrics Download lyrics 90 | --cover Download cover image 91 | --nomerge Don't sort and merge search results 92 | --filter TEXT Filter search results by file size and song duration 93 | --play Enable auto-play after downloading 94 | --help Show this message and exit. 95 | ``` 96 | 97 | - Default search sources are `qq netease kugou baidu`, with a limit of 5 results each, and the save directory is the current directory. 98 | - When specifying numbers, you can use formats like `1-5 7 10`. 99 | - By default, search results are sorted and deduplicated. Sorting is based on artist and song name, and when both are the same, the largest file is kept. 100 | - Lossless music tracks are limited in number. If lossless is not available, 320K or 128K will be displayed. 101 | - HTTP and SOCKS proxies are supported, in formats like `-x http://127.0.0.1:1087` or `-x socks5://127.0.0.1:1086`. 102 | 103 | - Attributes supported by filter include: 104 | - `size`: File size in MB 105 | - `length`: Song duration in seconds 106 | 107 | - Operators supported by filter include: 108 | - `>`: Greater than 109 | - `<`: Less than 110 | - `=`: Equal to 111 | 112 | ```bash 113 | music-dl -k --filter "size>8" # Only show songs larger than 8MB 114 | music-dl -k --filter "size<3" # Only show songs smaller than 3MB 115 | music-dl -k --filter "length>300" # Only show songs longer than 5 minutes 116 | music-dl -k --filter "length>180,length<300" # Only show songs between 3-5 minutes 117 | music-dl -k --filter "size>3,size<8,length>240" # Only show songs between 3-8MB and longer than 4 minutes 118 | ``` 119 | 120 | Example: 121 | 122 | ![](https://github.com/0xHJK/music-dl/raw/master/static/preview-en.png) 123 | 124 | ## Supported sources 125 | 126 | | Music sources | Abbreviation | Websites | 127 | | ------------------------- | ------------ | ------------------------- | 128 | | QQ Music | qq | | 129 | | Kugou Music | kugou | | 130 | | Netease Music | netease | | 131 | | Baidu Music | baidu | | 132 | | Xiami Music | xiami | | 133 | | Lossless Music From Baidu | flac | | 134 | 135 | Welcome to submit plugins to support more music sources! Refer to the files in `extractors`. 136 | 137 | ![](https://github.com/0xHJK/music-dl/raw/master/static/fork.png) 138 | 139 | ## Credits 140 | 141 | - 142 | - 143 | - 144 | - 145 | 146 | ## LICENSE 147 | 148 | [MIT License](https://github.com/0xHJK/music-dl/blob/master/LICENSE) 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music-dl: Listen to what you want 2 | 3 |

4 | 5 | music-dl 6 | 7 |

8 |
9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | **[Music-dl](https://github.com/0xHJK/music-dl)** is a command line tool which helps you search and download music from multiple sources. 24 | 25 | Support for QQ music, Netease music, Xiami music, Kugou music and Baidu music. See [supported sources](#支持的音乐源列表). 26 | 27 | **Python3 Only. Python 3.5+ Recommended.** 28 | 29 | [English](https://github.com/0xHJK/music-dl/blob/master/README.en.md) | 中文文档 30 | 31 | **[Music-dl](https://github.com/0xHJK/music-dl) 32 | **是一个基于Python3的命令行工具,可以从多个网站搜索和下载音乐,方便寻找音乐,解决不知道哪个网站有版权的问题。工具的本意是**聚合搜索**,API 33 | 是从公开的网络中获得,**不是破解版**,也听不了付费歌曲。 34 | 35 | **禁止将本工具用于商业用途**,如产生法律纠纷与本人无关,如有侵权,请联系我删除。 36 | 37 | 微博:[可乐芬达王老吉](https://weibo.com/p/1005056970125848/home?is_all=1) 38 | 39 | QQ群:[785798493](//shang.qq.com/wpa/qunwpa?idkey=ead6a77d50b8dbaa73cf78809aca0bd20c306b12f9984a17436f0342b1c0d65c) 40 | 41 | 最近API封杀有点多,个人有点维护不过来,需要大家帮忙更新。查看 [支持的音乐源列表](#支持的音乐源列表) 42 | 43 | > 注意: 部分音乐源在一些国家和地区不可用,可以考虑使用中国大陆代理。获取公共代理的方式可以参考我的另一个项目,两分钟获得数千个有效代理。 44 | 45 | ## 功能 46 | 47 | - 部分歌曲支持无损音乐 48 | - 优先搜索高品质音乐(无损 -> 320K -> 128K) 49 | - 支持 HTTP 和 SOCKS 代理 50 | - 支持多线程搜索 51 | - 支持搜索结果去重和排序 52 | - 支持搜索关键字高亮 53 | - 支持下载歌词和封面(部分) 54 | - 支持按文件大小和歌曲时长过滤搜索结果 (使用`--filter`参数) 55 | - 支持下载后自动播放功能 (使用`--play`参数) 56 | - 播放器支持常见控制功能:空格暂停/继续,左右切换歌曲,,/.前进后退5秒,q退出 57 | - 播放时可以用d键删除不喜欢的歌曲(从硬盘和播放列表中删除) 58 | 59 | > 注意:仅支持Python3,建议使用 **Python3.5 以上版本** 60 | 61 | ## 安装 62 | 63 | 使用pip安装(推荐,注意前面有一个`py`): 64 | 65 | ```bash 66 | $ pip3 install pymusic-dl 67 | ``` 68 | 69 | 手动安装(最新): 70 | 71 | ```bash 72 | $ git clone https://github.com/0xHJK/music-dl.git 73 | $ cd music-dl 74 | $ python3 setup.py install 75 | ``` 76 | 77 | 不安装直接运行: 78 | 79 | ```bash 80 | $ git clone https://github.com/0xHJK/music-dl.git 81 | $ cd music-dl 82 | $ pip3 install -r requirements.txt 83 | $ ./music-dl 84 | 85 | # 或 python3 music-dl 86 | ``` 87 | 88 | 在以下环境测试通过: 89 | 90 | | 系统名称 | 系统版本 | Python版本 | 91 | | -------- | -------------- | ---------- | 92 | | macOS | 10.14 | 3.7.0 | 93 | | macOS | 10.13 | 3.7.0 | 94 | | Windows | Windows 7 x64 | 3.7.2 | 95 | | Windows | Windows 10 x64 | 3.7.2 | 96 | | Ubuntu | 16.04 x64 | 3.5.2 | 97 | 98 | ## 使用方式 99 | 100 | v3.0预览版命令有较大的改变,建议先查看帮助 101 | 102 | ``` 103 | $ music-dl --help 104 | Usage: music-dl [OPTIONS] 105 | 106 | Search and download music from netease, qq, kugou, baidu and xiami. 107 | Example: music-dl -k "周杰伦" 108 | 109 | Options: 110 | --version Show the version and exit. 111 | -k, --keyword TEXT 搜索关键字,歌名和歌手同时输入可以提高匹配(如 空帆船 朴树) 112 | -u, --url TEXT 通过指定的歌曲URL下载音乐 113 | -p, --playlist TEXT 通过指定的歌单URL下载音乐 114 | -s, --source TEXT Supported music source: qq netease kugou baidu 115 | -n, --number INTEGER Number of search results 116 | -o, --outdir TEXT Output directory 117 | -x, --proxy TEXT Proxy (e.g. http://127.0.0.1:1087) 118 | -v, --verbose Verbose mode 119 | --lyrics 同时下载歌词 120 | --cover 同时下载封面 121 | --nomerge 不对搜索结果列表排序和去重 122 | --filter TEXT 按文件大小和歌曲时长过滤搜索结果 123 | --play 开启下载后自动播放功能 124 | --help Show this message and exit. 125 | ``` 126 | 127 | - 默认搜索`qq netease kugou baidu `,每个数量限制为5,保存目录为当前目录。 128 | - 指定序号时可以使用`1-5 7 10`的形式。 129 | - 默认对搜索结果排序和去重,排序顺序按照歌手和歌名排序,当两者都相同时保留最大的文件。 130 | - 无损音乐歌曲数量较少,如果没有无损会显示320K或128K。 131 | - 支持http代理和socks代理,格式形如`-x http://127.0.0.1:1087`或`-x socks5://127.0.0.1:1086` 132 | 133 | - filter支持的属性包括: 134 | - `size`: 文件大小,单位为MB 135 | - `length`: 歌曲时长,单位为秒 136 | 137 | - filter支持的操作符包括: 138 | - `>`: 大于 139 | - `<`: 小于 140 | - `=`: 等于 141 | 142 | ```bash 143 | music-dl -k --filter "size>8" #只显示大于8MB的歌曲 144 | music-dl -k --filter "size<3" #只显示小于3MB的歌曲 145 | music-dl -k --filter "length>300" #只显示大于5分钟的歌曲 146 | music-dl -k --filter "length>180,length<300" #只显示3-5分钟之间的歌曲 147 | music-dl -k --filter "size>3,size<8,length>240" #只显示3-8MB之间的歌曲,且时长超过4分钟 148 | ``` 149 | 150 | 示例: 151 | 152 | ![](https://github.com/0xHJK/music-dl/raw/master/static/preview.png) 153 | 154 | ## 支持的音乐源列表 155 | 156 | | 音乐源 | 缩写 | 网址 | 有效 | 无损 | 320K | 封面 | 歌词 | 歌单 | 单曲 | 157 | | ---------- | ------- | ------------------------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 158 | | QQ音乐 | qq | | ✓ | - | - | ✕ | ✓ | ✕ | ✕ | 159 | | 酷狗音乐 | kugou | | ✓ | - | - | - | ✕ | - | ✕ | 160 | | 网易云音乐 | netease | | ✓ | - | ✓ | ✓ | ✓ | ✓ | ✓ | 161 | | 咪咕音乐 | migu | | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | ✕ | 162 | | 百度音乐 | baidu | | ✓ | - | ✓ | ✓ | ✓ | ✕ | ✕ | 163 | | 虾米音乐 | xiami | | ✕ | - | - | - | - | ✕ | ✕ | 164 | 165 | > `-`表示不一定支持,`✓`表示部分或完全支持,`✕`表示尚未支持 166 | 167 | 欢迎提交插件支持更多音乐源!插件写法参考`addons`中的文件 168 | 169 | ![](https://github.com/0xHJK/music-dl/raw/master/static/fork.png) 170 | 171 | ## 更新记录 172 | 173 | - 2019-08-25 修复了QQ音乐、网易云音乐、酷狗音乐,新增咪咕音乐 174 | - 2019-08-03 修复了一些bug,屏蔽了不支持的源,目前仅百度音乐可用 175 | - 2019-06-13 重新增加虾米音乐高品质音乐支持,感谢群友0.0提供的API 176 | - 2019-06-11 v3.0预览版,代码重构,支持网易云音乐歌单和单曲下载,支持百度高品质音乐 177 | - 2019-04-08 发布v2.2.1版本 178 | - 2019-04-04 因为虾米音乐API变更,暂时屏蔽虾米搜索结果#22 179 | - 2019-04-02 修复#18和#21的BUG,优化显示效果,支持部分音乐源歌词和封面下载 180 | - 2019-03-11 开启默认支持所有音乐源,默认对搜索结果排序去重,优化显示效果,高亮搜索关键字和高品质音乐 181 | - 2019-02 完成部分翻译(英语、德语、日语、克罗地亚语)感谢@anomie31 @DarkLinkXXXX @terorie的帮助,目前翻译尚未完善,欢迎提交PR改进翻译 182 | - 2019-01-31 新增单元测试,集成发布,新增LOGO,新增小徽章,发布v2.1.0版本 183 | - 2019-01-28 重写一半以上代码,全面优化,发布到pip库,发布v2.0.0版本 184 | - 2019-01-26 支持http和socks代理,删除wget库,新增click库,发布v1.1版 185 | - 2019-01-25 支持百度无损音乐 186 | - 2019-01-24 优化交互、修复bug 187 | - 2019-01-22 解决Windows兼容问题,支持多线程,发布v1.0版 188 | - 2019-01-21 支持虾米音乐,支持去重 189 | - 2019-01-20 支持百度音乐 190 | - 2019-01-17 支持指定目录、数量、音乐源 191 | - 2019-01-12 QQ音乐320K失效 192 | - 2019-01-11 支持网易云音乐 193 | - 2019-01-09 完成v0.1版,支持酷狗和QQ 194 | 195 | ## 提Issues说明 196 | 197 | - **检查是否是最新的代码,检查是否是Python3.5+,检查依赖有没有安装完整**。 198 | - 说明使用的操作系统,例如Windows 10 x64 199 | - 说明Python版本,以及是否使用了pyenv等虚拟环境 200 | - 说明使用的命令参数、搜索关键字和出错的音乐源 201 | - 使用`-v`参数重试,说明详细的错误信息,最好有截图 202 | - 如果有新的思路和建议也欢迎提交 203 | 204 | ## Credits 致谢 205 | 206 | 本项目受以下项目启发,参考了其中一部分思路,向这些开发者表示感谢。 207 | 208 | - 209 | - 210 | - 211 | - 212 | - 213 | 214 | ## 用爱发电 215 | 216 | 维护不易,欢迎扫描恰饭二维码 217 | 218 | ![](https://github.com/0xHJK/music-dl/raw/master/static/wepay.jpg) 219 | 220 | ## LICENSE 221 | 222 | [MIT License](https://github.com/0xHJK/music-dl/blob/master/LICENSE) 223 | -------------------------------------------------------------------------------- /music_dl/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @file: player.py 5 | @description: 音乐播放器功能 6 | """ 7 | 8 | import os 9 | import sys 10 | import time 11 | import gettext 12 | import click 13 | 14 | # 导入用于播放音乐的库 15 | try: 16 | import pygame 17 | PYGAME_AVAILABLE = True 18 | except ImportError: 19 | PYGAME_AVAILABLE = False 20 | 21 | gettext.install("music-dl", "locale") 22 | 23 | # 导入工具函数 24 | from .utils import colorize 25 | 26 | 27 | def time_duration_to_seconds(duration): 28 | """将时间字符串(如'05:30')转换为秒数""" 29 | if not duration: 30 | return 0 31 | 32 | parts = duration.split(":") 33 | if len(parts) == 3: # H:M:S 34 | return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) 35 | elif len(parts) == 2: # M:S 36 | return int(parts[0]) * 60 + int(parts[1]) 37 | else: # S 38 | return int(parts[0]) 39 | 40 | 41 | def play_music(song_files): 42 | """播放下载的音乐文件""" 43 | if not PYGAME_AVAILABLE: 44 | click.echo(_("警告: 未安装pygame库,无法播放音乐。请安装pygame后重试: pip install pygame")) 45 | return 46 | 47 | # 过滤有效文件 48 | valid_song_files = [f for f in song_files if f and os.path.exists(f)] 49 | if not valid_song_files: 50 | click.echo(_("没有有效的音乐文件可播放")) 51 | return 52 | 53 | # 初始化 54 | pygame.init() 55 | pygame.mixer.init() 56 | playing = True 57 | current_song_index = 0 58 | current_pos = 0 59 | 60 | # 显示帮助 61 | click.echo(_("\n播放控制: 空格键-暂停/继续 左右方向键-切换歌曲 ,键-后退5秒 .键-前进5秒 q键-退出播放 d键-删除这首歌\n")) 62 | 63 | # 播放第一首 64 | click.echo(_("\n正在播放: ") + colorize(os.path.basename(valid_song_files[current_song_index]), "highlight")) 65 | pygame.mixer.music.load(valid_song_files[current_song_index]) 66 | pygame.mixer.music.play() 67 | 68 | # 检测键盘输入环境 69 | try: 70 | import msvcrt 71 | is_windows = True 72 | except ImportError: 73 | is_windows = False 74 | try: 75 | import termios, tty 76 | except ImportError: 77 | click.echo(_("警告: 不支持的操作系统,无法捕获键盘输入,将自动播放全部歌曲")) 78 | while pygame.mixer.music.get_busy(): 79 | time.sleep(1) 80 | return 81 | 82 | # 设置播放位置 83 | def set_position(seconds): 84 | nonlocal current_pos 85 | try: 86 | sound = pygame.mixer.Sound(valid_song_files[current_song_index]) 87 | song_length = sound.get_length() 88 | except: 89 | song_length = 300 90 | 91 | pygame.mixer.music.stop() 92 | new_pos = max(0, min(song_length, seconds)) 93 | current_pos = new_pos 94 | pygame.mixer.music.load(valid_song_files[current_song_index]) 95 | pygame.mixer.music.play(start=new_pos) 96 | 97 | # 删除当前歌曲 98 | def delete_current_song(): 99 | nonlocal current_song_index, valid_song_files 100 | 101 | current_song_file = valid_song_files[current_song_index] 102 | pygame.mixer.music.stop() 103 | pygame.mixer.quit() 104 | pygame.mixer.init() 105 | time.sleep(0.2) 106 | 107 | try: 108 | os.remove(current_song_file) 109 | click.echo(_("\n已删除: ") + colorize(os.path.basename(current_song_file), "red")) 110 | valid_song_files.pop(current_song_index) 111 | 112 | if not valid_song_files: 113 | click.echo(_("\n播放列表为空,退出播放")) 114 | return True 115 | 116 | if current_song_index >= len(valid_song_files): 117 | current_song_index = max(0, len(valid_song_files) - 1) 118 | 119 | current_pos = 0 120 | click.echo(_("\n正在播放: ") + colorize(os.path.basename(valid_song_files[current_song_index]), "highlight")) 121 | pygame.mixer.music.load(valid_song_files[current_song_index]) 122 | pygame.mixer.music.play() 123 | except Exception as e: 124 | click.echo(_("\n删除文件失败: ") + str(e)) 125 | return False 126 | 127 | # 获取键盘输入 128 | def get_key(): 129 | if is_windows: 130 | if msvcrt.kbhit(): 131 | key = msvcrt.getch() 132 | if key == b' ': 133 | return 'space' 134 | elif key == b'q': 135 | return 'q' 136 | elif key == b',': 137 | return 'comma' 138 | elif key == b'.': 139 | return 'dot' 140 | elif key == b'd': 141 | return 'delete' 142 | elif key == b'\xe0': 143 | key = msvcrt.getch() 144 | if key == b'K': 145 | return 'left' 146 | elif key == b'M': 147 | return 'right' 148 | return None 149 | else: 150 | fd = sys.stdin.fileno() 151 | old_settings = termios.tcgetattr(fd) 152 | try: 153 | tty.setraw(fd) 154 | ch = sys.stdin.read(1) 155 | finally: 156 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 157 | 158 | if ch == ' ': 159 | return 'space' 160 | elif ch == 'q': 161 | return 'q' 162 | elif ch == ',': 163 | return 'comma' 164 | elif ch == '.': 165 | return 'dot' 166 | elif ch == 'd': 167 | return 'delete' 168 | elif ch == '\x1b': 169 | ch = sys.stdin.read(2) 170 | if ch == '[D': 171 | return 'left' 172 | elif ch == '[C': 173 | return 'right' 174 | return None 175 | 176 | # 主循环 177 | while True: 178 | time.sleep(0.1) 179 | 180 | if playing and pygame.mixer.music.get_busy(): 181 | current_pos += 0.1 182 | 183 | # 播放下一首 184 | if not pygame.mixer.music.get_busy() and playing: 185 | current_song_index += 1 186 | if current_song_index >= len(valid_song_files): 187 | click.echo(_("\n播放列表已播放完毕")) 188 | break 189 | 190 | current_pos = 0 191 | click.echo(_("\n正在播放: ") + colorize(os.path.basename(valid_song_files[current_song_index]), "highlight")) 192 | pygame.mixer.music.load(valid_song_files[current_song_index]) 193 | pygame.mixer.music.play() 194 | 195 | # 处理按键 196 | key = get_key() 197 | if key == 'space': 198 | if playing: 199 | pygame.mixer.music.pause() 200 | playing = False 201 | else: 202 | pygame.mixer.music.unpause() 203 | playing = True 204 | 205 | elif key == 'left': 206 | current_song_index = max(0, current_song_index - 1) 207 | current_pos = 0 208 | click.echo(_("\n正在播放: ") + colorize(os.path.basename(valid_song_files[current_song_index]), "highlight")) 209 | pygame.mixer.music.load(valid_song_files[current_song_index]) 210 | pygame.mixer.music.play() 211 | playing = True 212 | 213 | elif key == 'right': 214 | current_song_index = min(len(valid_song_files) - 1, current_song_index + 1) 215 | current_pos = 0 216 | click.echo(_("\n正在播放: ") + colorize(os.path.basename(valid_song_files[current_song_index]), "highlight")) 217 | pygame.mixer.music.load(valid_song_files[current_song_index]) 218 | pygame.mixer.music.play() 219 | playing = True 220 | 221 | elif key == 'comma': 222 | set_position(current_pos - 5) 223 | playing = True 224 | 225 | elif key == 'dot': 226 | set_position(current_pos + 5) 227 | playing = True 228 | 229 | elif key == 'delete': 230 | should_exit = delete_current_song() 231 | if should_exit: 232 | break 233 | playing = True 234 | 235 | elif key == 'q': 236 | pygame.mixer.music.stop() 237 | break 238 | 239 | pygame.mixer.quit() 240 | pygame.quit() 241 | click.echo(_("\n播放结束")) -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import sys 5 | import os 6 | import pytest 7 | 8 | # 将项目根目录添加到 Python 路径 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | from music_dl import config 12 | from music_dl.song import BasicSong 13 | 14 | 15 | class TestFilter: 16 | def setup_method(self): 17 | # 初始化配置 18 | config.init() 19 | 20 | # 创建测试用的歌曲列表 21 | self.song1 = BasicSong() 22 | self.song1.size = 2.5 # 2.5MB 23 | self.song1._duration = "0:02:30" # 2分30秒 = 150秒 24 | 25 | self.song2 = BasicSong() 26 | self.song2.size = 8.1 # 8.1MB 27 | self.song2._duration = "0:04:15" # 4分15秒 = 255秒 28 | 29 | self.song3 = BasicSong() 30 | self.song3.size = 15.3 # 15.3MB 31 | self.song3._duration = "0:08:45" # 8分45秒 = 525秒 32 | 33 | self.test_songs = [self.song1, self.song2, self.song3] 34 | 35 | def apply_filter(self, songs, filter_str): 36 | """应用过滤条件,模拟 __main__.py 中的过滤逻辑""" 37 | filtered_songs = songs 38 | 39 | if not filter_str: 40 | return filtered_songs 41 | 42 | # 解析过滤条件 43 | filter_conditions = filter_str.split(',') 44 | 45 | for condition in filter_conditions: 46 | if not condition: 47 | continue 48 | 49 | result_songs = [] 50 | 51 | # 大小过滤 52 | if condition.startswith("size"): 53 | if ">" in condition: 54 | # 大于指定大小 55 | size_limit = float(condition.split(">")[1]) 56 | for song in filtered_songs: 57 | if song.size and song.size > size_limit: 58 | result_songs.append(song) 59 | elif not song.size: # 如果没有大小信息,保留 60 | result_songs.append(song) 61 | elif "<" in condition: 62 | # 小于指定大小 63 | size_limit = float(condition.split("<")[1]) 64 | for song in filtered_songs: 65 | if song.size and song.size < size_limit: 66 | result_songs.append(song) 67 | elif not song.size: # 如果没有大小信息,保留 68 | result_songs.append(song) 69 | elif "=" in condition: 70 | # 等于指定大小 71 | size_equal = float(condition.split("=")[1]) 72 | for song in filtered_songs: 73 | if song.size and abs(song.size - size_equal) < 0.01: # 允许0.01MB的误差 74 | result_songs.append(song) 75 | elif not song.size: # 如果没有大小信息,保留 76 | result_songs.append(song) 77 | 78 | # 长度过滤 79 | elif condition.startswith("length"): 80 | if ">" in condition: 81 | # 大于指定长度 82 | length_limit = int(condition.split(">")[1]) 83 | for song in filtered_songs: 84 | if song._duration: # 确保有时长信息 85 | duration_parts = song._duration.split(":") 86 | # 转换为秒 87 | if len(duration_parts) == 3: # H:M:S 88 | duration_seconds = int(duration_parts[0]) * 3600 + int(duration_parts[1]) * 60 + int(duration_parts[2]) 89 | elif len(duration_parts) == 2: # M:S 90 | duration_seconds = int(duration_parts[0]) * 60 + int(duration_parts[1]) 91 | else: # S 92 | duration_seconds = int(duration_parts[0]) 93 | 94 | if duration_seconds > length_limit: 95 | result_songs.append(song) 96 | else: 97 | result_songs.append(song) # 如果没有时长信息,保留 98 | elif "<" in condition: 99 | # 小于指定长度 100 | length_limit = int(condition.split("<")[1]) 101 | for song in filtered_songs: 102 | if song._duration: # 确保有时长信息 103 | duration_parts = song._duration.split(":") 104 | # 转换为秒 105 | if len(duration_parts) == 3: # H:M:S 106 | duration_seconds = int(duration_parts[0]) * 3600 + int(duration_parts[1]) * 60 + int(duration_parts[2]) 107 | elif len(duration_parts) == 2: # M:S 108 | duration_seconds = int(duration_parts[0]) * 60 + int(duration_parts[1]) 109 | else: # S 110 | duration_seconds = int(duration_parts[0]) 111 | 112 | if duration_seconds < length_limit: 113 | result_songs.append(song) 114 | else: 115 | result_songs.append(song) # 如果没有时长信息,保留 116 | elif "=" in condition: 117 | # 等于指定长度 118 | length_equal = int(condition.split("=")[1]) 119 | for song in filtered_songs: 120 | if song._duration: # 确保有时长信息 121 | duration_parts = song._duration.split(":") 122 | # 转换为秒 123 | if len(duration_parts) == 3: # H:M:S 124 | duration_seconds = int(duration_parts[0]) * 3600 + int(duration_parts[1]) * 60 + int(duration_parts[2]) 125 | elif len(duration_parts) == 2: # M:S 126 | duration_seconds = int(duration_parts[0]) * 60 + int(duration_parts[1]) 127 | else: # S 128 | duration_seconds = int(duration_parts[0]) 129 | 130 | if duration_seconds == length_equal: 131 | result_songs.append(song) 132 | else: 133 | result_songs.append(song) # 如果没有时长信息,保留 134 | 135 | # 更新歌曲列表,用于下一个条件过滤 136 | filtered_songs = result_songs 137 | 138 | return filtered_songs 139 | 140 | def test_size_filter_greater_than(self): 141 | """测试大小过滤 - 大于指定值""" 142 | filtered = self.apply_filter(self.test_songs, "size>10") 143 | assert len(filtered) == 1 144 | assert filtered[0] == self.song3 145 | 146 | def test_size_filter_less_than(self): 147 | """测试大小过滤 - 小于指定值""" 148 | filtered = self.apply_filter(self.test_songs, "size<5") 149 | assert len(filtered) == 1 150 | assert filtered[0] == self.song1 151 | 152 | def test_size_filter_equal(self): 153 | """测试大小过滤 - 等于指定值""" 154 | filtered = self.apply_filter(self.test_songs, "size=8.1") 155 | assert len(filtered) == 1 156 | assert filtered[0] == self.song2 157 | 158 | def test_length_filter_greater_than(self): 159 | """测试长度过滤 - 大于指定值""" 160 | filtered = self.apply_filter(self.test_songs, "length>300") 161 | assert len(filtered) == 1 162 | assert filtered[0] == self.song3 163 | 164 | def test_length_filter_less_than(self): 165 | """测试长度过滤 - 小于指定值""" 166 | filtered = self.apply_filter(self.test_songs, "length<200") 167 | assert len(filtered) == 1 168 | assert filtered[0] == self.song1 169 | 170 | def test_combined_filters(self): 171 | """测试组合过滤条件""" 172 | filtered = self.apply_filter(self.test_songs, "size>5,length<300") 173 | assert len(filtered) == 1 174 | assert filtered[0] == self.song2 175 | 176 | def test_combined_filters_complex(self): 177 | """测试复杂组合过滤条件""" 178 | filtered = self.apply_filter(self.test_songs, "size>2,size<10,length>200") 179 | assert len(filtered) == 1 180 | assert filtered[0] == self.song2 181 | 182 | def test_range_filter(self): 183 | """测试范围过滤(通过组合大于小于条件)""" 184 | filtered = self.apply_filter(self.test_songs, "size>5,size<10") 185 | assert len(filtered) == 1 186 | assert filtered[0] == self.song2 -------------------------------------------------------------------------------- /music_dl/addons/netease.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: netease.py 6 | @time: 2019-05-08 7 | """ 8 | 9 | import os 10 | import re 11 | import binascii 12 | import base64 13 | import json 14 | import copy 15 | from Crypto.Cipher import AES 16 | from .. import config 17 | from ..api import MusicApi 18 | from ..exceptions import RequestError, ResponseError, DataError 19 | from ..song import BasicSong 20 | 21 | __all__ = ["search", "playlist"] 22 | 23 | 24 | class NeteaseApi(MusicApi): 25 | """ Netease music api http://music.163.com """ 26 | 27 | session = copy.deepcopy(MusicApi.session) 28 | session.headers.update({"referer": "http://music.163.com/"}) 29 | 30 | @classmethod 31 | def encode_netease_data(cls, data) -> str: 32 | data = json.dumps(data) 33 | key = binascii.unhexlify("7246674226682325323F5E6544673A51") 34 | encryptor = AES.new(key, AES.MODE_ECB) 35 | # 补足data长度,使其是16的倍数 36 | pad = 16 - len(data) % 16 37 | fix = chr(pad) * pad 38 | byte_data = (data + fix).encode("utf-8") 39 | return binascii.hexlify(encryptor.encrypt(byte_data)).upper().decode() 40 | 41 | @classmethod 42 | def encrypted_request(cls, data) -> dict: 43 | MODULUS = ( 44 | "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7" 45 | "b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280" 46 | "104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932" 47 | "575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b" 48 | "3ece0462db0a22b8e7" 49 | ) 50 | PUBKEY = "010001" 51 | NONCE = b"0CoJUm6Qyw8W8jud" 52 | data = json.dumps(data).encode("utf-8") 53 | secret = cls.create_key(16) 54 | params = cls.aes(cls.aes(data, NONCE), secret) 55 | encseckey = cls.rsa(secret, PUBKEY, MODULUS) 56 | return {"params": params, "encSecKey": encseckey} 57 | 58 | @classmethod 59 | def aes(cls, text, key): 60 | pad = 16 - len(text) % 16 61 | text = text + bytearray([pad] * pad) 62 | encryptor = AES.new(key, 2, b"0102030405060708") 63 | ciphertext = encryptor.encrypt(text) 64 | return base64.b64encode(ciphertext) 65 | 66 | @classmethod 67 | def rsa(cls, text, pubkey, modulus): 68 | text = text[::-1] 69 | rs = pow(int(binascii.hexlify(text), 16), int(pubkey, 16), int(modulus, 16)) 70 | return format(rs, "x").zfill(256) 71 | 72 | @classmethod 73 | def create_key(cls, size): 74 | return binascii.hexlify(os.urandom(size))[:16] 75 | 76 | 77 | class NeteaseSong(BasicSong): 78 | def __init__(self): 79 | super(NeteaseSong, self).__init__() 80 | 81 | def download_lyrics(self): 82 | row_data = {"csrf_token": "", "id": self.id, "lv": -1, "tv": -1} 83 | data = NeteaseApi.encrypted_request(row_data) 84 | 85 | self.lyrics_text = ( 86 | NeteaseApi.request( 87 | "https://music.163.com/weapi/song/lyric", method="POST", data=data 88 | ) 89 | .get("lrc", {}) 90 | .get("lyric", "") 91 | ) 92 | 93 | if self.lyrics_text: 94 | super(NeteaseSong, self)._save_lyrics_text() 95 | 96 | def download(self): 97 | """ Download song from netease music """ 98 | data = NeteaseApi.encrypted_request(dict(ids=[self.id], br=32000)) 99 | res_data = NeteaseApi.request( 100 | "http://music.163.com/weapi/song/enhance/player/url", 101 | method="POST", 102 | data=data, 103 | ).get("data", []) 104 | 105 | if len(res_data) > 0: 106 | self.song_url = res_data[0].get("url", "") 107 | self.rate = int(res_data[0].get("br", 0) / 1000) 108 | 109 | super(NeteaseSong, self).download() 110 | 111 | 112 | def netease_search(keyword) -> list: 113 | """ Search song from netease music """ 114 | number = config.get("number") or 5 115 | eparams = { 116 | "method": "POST", 117 | "url": "http://music.163.com/api/cloudsearch/pc", 118 | "params": {"s": keyword, "type": 1, "offset": 0, "limit": number}, 119 | } 120 | data = {"eparams": NeteaseApi.encode_netease_data(eparams)} 121 | 122 | songs_list = [] 123 | res_data = ( 124 | NeteaseApi.request( 125 | "http://music.163.com/api/linux/forward", method="POST", data=data 126 | ) 127 | .get("result", {}) 128 | .get("songs", {}) 129 | ) 130 | try: 131 | for item in res_data: 132 | if item.get("privilege", {}).get("fl", {}) == 0: 133 | # 没有版权 134 | continue 135 | # 获得歌手名字 136 | singers = [s.get("name", "") for s in item.get("ar", [])] 137 | # 获得音乐的文件大小 138 | # TODO: 获取到的大小并不准确,考虑逐一获取歌曲详情 139 | if item.get("privilege", {}).get("fl", {}) >= 320000 and item.get("h", ""): 140 | size = item.get("h", {}).get("size", 0) 141 | elif item.get("privilege", {}).get("fl", {}) >= 192000 and item.get( 142 | "m", "" 143 | ): 144 | size = item.get("m", {}).get("size", 0) 145 | else: 146 | size = item.get("l", {}).get("size", 0) 147 | 148 | song = NeteaseSong() 149 | song.source = "netease" 150 | song.id = item.get("id", "") 151 | song.title = item.get("name", "") 152 | song.singer = "、".join(singers) 153 | song.album = item.get("al", {}).get("name", "") 154 | song.duration = int(item.get("dt", 0) / 1000) 155 | song.size = round(size / 1048576, 2) 156 | song.cover_url = item.get("al", {}).get("picUrl", "") 157 | songs_list.append(song) 158 | except Exception as e: 159 | raise DataError(e) 160 | 161 | return songs_list 162 | 163 | 164 | def netease_playlist(url) -> list: 165 | songs_list = [] 166 | playlist_id = re.findall(r".+playlist\\*\?id\\*=(\d+)", url)[0] 167 | if playlist_id: 168 | params = dict( 169 | id=playlist_id, total="true", limit=1000, n=1000, offest=0, csrf_token="" 170 | ) 171 | data = NeteaseApi.encrypted_request(params) 172 | 173 | res_data = ( 174 | NeteaseApi.request( 175 | "http://music.163.com/weapi/v3/playlist/detail", 176 | method="POST", 177 | data=data, 178 | ) 179 | .get("playlist", {}) 180 | .get("tracks", []) 181 | ) 182 | for item in res_data: 183 | song = NeteaseSong() 184 | # 获得歌手名字 185 | singers = [s.get("name", "") for s in item.get("ar", {})] 186 | # 获得音乐文件大小 187 | # TODO: 获取到的大小并不准确,考虑逐一获取歌曲详情 188 | if item.get("l", ""): 189 | size = item.get("l", {}).get("size", 0) 190 | elif item.get("m", ""): 191 | size = item.get("m", {}).get("size", 0) 192 | else: 193 | size = item.get("h", {}).get("size", 0) 194 | song.source = "netease" 195 | song.id = item.get("id", "") 196 | song.title = item.get("name", "") 197 | song.singer = "、".join(singers) 198 | song.album = item.get("al", {}).get("name", "") 199 | song.duration = int(item.get("dt", 0) / 1000) 200 | song.size = round(size / 1048576, 2) 201 | song.cover_url = item.get("al", {}).get("picUrl", "") 202 | songs_list.append(song) 203 | return songs_list 204 | 205 | 206 | def netease_single(url) -> NeteaseSong: 207 | song_id = re.findall(r".+song\\*\?id\\*=(\d+)", url)[0] 208 | data_detail = NeteaseApi.encrypted_request( 209 | dict(c=json.dumps([{"id": song_id}]), ids=[song_id]) 210 | ) 211 | res_data_detail = NeteaseApi.request( 212 | "http://music.163.com/weapi/v3/song/detail", method="POST", data=data_detail 213 | ).get("songs", []) 214 | if len(res_data_detail) > 0: 215 | item = res_data_detail[0] 216 | song = NeteaseSong() 217 | song.source = "netease" 218 | song.id = item.get("id", "") 219 | song.title = item.get("name", "") 220 | singers = [s.get("name", "") for s in item.get("ar", {})] 221 | song.singer = "、".join(singers) 222 | song.album = item.get("al", {}).get("name", "") 223 | song.duration = int(item.get("dt", 0) / 1000) 224 | song.cover_url = item.get("al", {}).get("picUrl", "") 225 | return song 226 | else: 227 | raise DataError("Get song detail failed.") 228 | 229 | 230 | search = netease_search 231 | playlist = netease_playlist 232 | single = netease_single 233 | -------------------------------------------------------------------------------- /music_dl/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 _*- 3 | """ 4 | @author: HJK 5 | @file: main.py 6 | @time: 2019-01-08 7 | """ 8 | 9 | import sys 10 | import os 11 | import re 12 | import gettext 13 | import click 14 | import logging 15 | import prettytable as pt 16 | from . import config 17 | from .utils import colorize 18 | from .source import MusicSource 19 | from .player import play_music, time_duration_to_seconds 20 | 21 | gettext.install("music-dl", "locale") 22 | 23 | 24 | def menu(songs_list): 25 | # 创建table 26 | tb = pt.PrettyTable() 27 | tb.field_names = ["序号", "歌名", "歌手", "大小", "时长", "专辑", "来源"] 28 | # 遍历输出搜索列表 29 | songs_list.sort(key=lambda x: x.size) 30 | for index, song in enumerate(songs_list): 31 | song.idx = index 32 | tb.add_row(song.row) 33 | # click.echo(song.info) 34 | tb.align = "l" 35 | click.echo(tb) 36 | click.echo("") 37 | 38 | # 用户指定下载序号 39 | prompt = ( 40 | _("请输入{下载序号},支持形如 {numbers} 的格式,输入 {N} 跳过下载").format( 41 | 下载序号=colorize(_("下载序号"), "yellow"), 42 | numbers=colorize("0 3-5 8", "yellow"), 43 | N=colorize("N", "yellow"), 44 | ) 45 | + "\n >>" 46 | ) 47 | 48 | choices = click.prompt(prompt) 49 | 50 | while not re.match(r"^((\d+\-\d+)|(\d+)|\s+)+$", choices): 51 | if choices.lower() == "n": 52 | return 53 | choices = click.prompt("%s%s" % (colorize(_("输入有误!"), "red"), prompt)) 54 | 55 | click.echo("") 56 | selected_list = [] 57 | downloaded_files = [] 58 | 59 | for choice in choices.split(): 60 | start, to, end = choice.partition("-") 61 | if end: 62 | selected_list += range(int(start), int(end) + 1) 63 | else: 64 | selected_list.append(int(start)) 65 | 66 | for idx in selected_list: 67 | if idx < len(songs_list): 68 | song = songs_list[idx] 69 | # 记录下载前的文件不存在状态 70 | song_file_exists = os.path.exists(song.song_fullname) if hasattr(song, 'song_fullname') else False 71 | 72 | # 下载歌曲 73 | song.download() 74 | 75 | # 确认文件被下载成功(通过检查文件是否存在且下载前不存在) 76 | if hasattr(song, 'song_fullname') and os.path.exists(song.song_fullname) and (song.song_url or not song_file_exists): 77 | downloaded_files.append(song.song_fullname) 78 | 79 | # 如果启用了播放功能,则播放下载的音乐 80 | if config.get("play") and downloaded_files: 81 | play_music(downloaded_files) 82 | 83 | 84 | def run(): 85 | ms = MusicSource() 86 | if config.get("keyword"): 87 | songs_list = ms.search(config.get("keyword"), config.get("source").split()) 88 | 89 | # 处理过滤条件 90 | filter_str = config.get("filter") 91 | if filter_str: 92 | for condition in filter_str.split(','): 93 | if not condition: 94 | continue 95 | 96 | # 解析条件 97 | filtered_songs = [] 98 | 99 | if condition.startswith("size"): 100 | # 解析大小条件 101 | if ">" in condition: 102 | size_limit = float(condition.split(">")[1]) 103 | filtered_songs = [song for song in songs_list if 104 | (song.size and song.size > size_limit) or not song.size] 105 | elif "<" in condition: 106 | size_limit = float(condition.split("<")[1]) 107 | filtered_songs = [song for song in songs_list if 108 | (song.size and song.size < size_limit) or not song.size] 109 | elif "=" in condition: 110 | size_equal = float(condition.split("=")[1]) 111 | filtered_songs = [song for song in songs_list if 112 | (song.size and abs(song.size - size_equal) < 0.01) or not song.size] 113 | 114 | elif condition.startswith("length"): 115 | # 解析时长条件 116 | if ">" in condition: 117 | length_limit = int(condition.split(">")[1]) 118 | filtered_songs = [song for song in songs_list if 119 | not song._duration or time_duration_to_seconds(song._duration) > length_limit] 120 | elif "<" in condition: 121 | length_limit = int(condition.split("<")[1]) 122 | filtered_songs = [song for song in songs_list if 123 | not song._duration or time_duration_to_seconds(song._duration) < length_limit] 124 | elif "=" in condition: 125 | length_equal = int(condition.split("=")[1]) 126 | filtered_songs = [song for song in songs_list if 127 | not song._duration or time_duration_to_seconds(song._duration) == length_equal] 128 | 129 | # 更新歌曲列表 130 | songs_list = filtered_songs 131 | 132 | menu(songs_list) 133 | config.set("keyword", click.prompt(_("请输入要搜索的歌曲,或Ctrl+C退出") + "\n >>")) 134 | run() 135 | elif config.get("playlist"): 136 | songs_list = ms.playlist(config.get("playlist")) 137 | menu(songs_list) 138 | elif config.get("url"): 139 | song = ms.single(config.get("url")) 140 | # 记录下载前的文件不存在状态 141 | song_file_exists = os.path.exists(song.song_fullname) if hasattr(song, 'song_fullname') else False 142 | 143 | # 下载歌曲 144 | song.download() 145 | 146 | # 如果启用了播放功能,则播放下载的音乐 147 | if config.get("play") and hasattr(song, 'song_fullname') and os.path.exists(song.song_fullname) and (song.song_url or not song_file_exists): 148 | play_music([song.song_fullname]) 149 | else: 150 | return 151 | 152 | 153 | @click.command() 154 | @click.version_option() 155 | @click.option("-k", "--keyword", help=_("搜索关键字,歌名和歌手同时输入可以提高匹配(如 空帆船 朴树)")) 156 | @click.option("-u", "--url", default="", help=_("通过指定的歌曲URL下载音乐")) 157 | @click.option("-p", "--playlist", default="", help=_("通过指定的歌单URL下载音乐")) 158 | @click.option( 159 | "-s", 160 | "--source", 161 | # default="qq netease kugou baidu", 162 | help=_("支持的数据源: ") + "baidu", 163 | ) 164 | @click.option("-n", "--number", default=5, help=_("搜索数量限制")) 165 | @click.option("-o", "--outdir", default=".", help=_("指定输出目录")) 166 | @click.option("-x", "--proxy", default="", help=_("指定代理(如http://127.0.0.1:1087)")) 167 | @click.option("-v", "--verbose", default=False, is_flag=True, help=_("详细模式")) 168 | @click.option("--lyrics", default=False, is_flag=True, help=_("同时下载歌词")) 169 | @click.option("--cover", default=False, is_flag=True, help=_("同时下载封面")) 170 | @click.option("--nomerge", default=False, is_flag=True, help=_("不对搜索结果列表排序和去重")) 171 | @click.option("--filter", default="", help=_("过滤条件,如'size>8,length>300'表示大于8MB且时长超过5分钟")) 172 | @click.option("--play", default=False, is_flag=True, help=_("下载完成后播放音乐")) 173 | def main( 174 | keyword, 175 | url, 176 | playlist, 177 | source, 178 | number, 179 | outdir, 180 | proxy, 181 | verbose, 182 | lyrics, 183 | cover, 184 | nomerge, 185 | filter, 186 | play, 187 | ): 188 | """ 189 | Search and download music from netease, qq, kugou, baidu and xiami. 190 | Example: music-dl -k "周杰伦" 191 | """ 192 | if sum([bool(keyword), bool(url), bool(playlist)]) != 1: 193 | # click.echo(_("ERROR: 必须指定搜索关键字、歌曲的URL或歌单的URL中的一个") + "\n", err=True) 194 | # ctx = click.get_current_context() 195 | # click.echo(ctx.get_help()) 196 | # ctx.exit() 197 | keyword = click.prompt(_("搜索关键字,歌名和歌手同时输入可以提高匹配(如 空帆船 朴树)") + "\n >>") 198 | 199 | # 初始化全局变量 200 | config.init() 201 | config.set("keyword", keyword) 202 | config.set("url", url) 203 | config.set("playlist", playlist) 204 | if source: 205 | config.set("source", source) 206 | config.set("number", min(number, 50)) 207 | config.set("outdir", outdir) 208 | config.set("verbose", verbose) 209 | config.set("lyrics", lyrics) 210 | config.set("cover", cover) 211 | config.set("nomerge", nomerge) 212 | config.set("filter", filter) 213 | config.set("play", play) 214 | if proxy: 215 | proxies = {"http": proxy, "https": proxy} 216 | config.set("proxies", proxies) 217 | 218 | level = logging.INFO if verbose else logging.WARNING 219 | logging.basicConfig( 220 | level=level, 221 | format="[%(asctime)s] %(levelname)-8s | %(name)s: %(msg)s ", 222 | datefmt="%H:%M:%S", 223 | ) 224 | 225 | try: 226 | run() 227 | except (EOFError, KeyboardInterrupt): 228 | sys.exit(0) 229 | 230 | 231 | if __name__ == "__main__": 232 | main() 233 | -------------------------------------------------------------------------------- /music_dl/song.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @author: HJK 5 | @file: basic.py 6 | @time: 2019-05-07 7 | """ 8 | 9 | """ 10 | Basic song object 11 | """ 12 | 13 | import os 14 | import re 15 | import datetime 16 | import logging 17 | import click 18 | import requests 19 | from . import config 20 | from .utils import colorize 21 | 22 | 23 | class BasicSong: 24 | """ 25 | Define the basic properties and methods of a song. 26 | Such as title, name, singer etc. 27 | """ 28 | 29 | def __init__(self): 30 | self.idx = 0 31 | self.id = 0 32 | self._title = "" 33 | self._singer = "" 34 | self.ext = "mp3" 35 | self.album = "" 36 | self.size = "" 37 | self.rate = "" 38 | self._duration = "" 39 | self.source = "" 40 | self._song_url = "" 41 | # self.song_file = "" 42 | self.cover_url = "" 43 | # self.cover_file = "" 44 | self.lyrics_url = "" 45 | self.lyrics_text = "" 46 | # self.lyrics_file = "" 47 | self._fullname = "" 48 | self.logger = logging.getLogger(__name__) 49 | 50 | def __repr__(self): 51 | """ Abstract of the song """ 52 | source = colorize("%s" % self.source.upper(), self.source) 53 | return "%s #%s %s-%s-%s \n %s \n" % ( 54 | source, 55 | self.id, 56 | self.title, 57 | self.singer, 58 | self.album, 59 | self.song_url, 60 | ) 61 | 62 | def __str__(self): 63 | """ Song details """ 64 | source = colorize("%s" % self.source.upper(), self.source) 65 | return _( 66 | " -> Source: {source} #{id}\n" 67 | " -> Title: {title}\n" 68 | " -> Singer: {singer}\n" 69 | " -> Album: {album}\n" 70 | " -> Duration: {duration}\n" 71 | " -> Size: {size}MB\n" 72 | " -> Bit Rate: {rate}\n" 73 | " -> Song URL: {song_url}\n" 74 | " -> Lyrics URL: {lyrics_url}\n" 75 | " -> Cover URL: {cover_url}\n" 76 | ).format( 77 | source=source, 78 | id=self.id, 79 | title=self.title, 80 | singer=self.singer, 81 | album=self.album, 82 | duration=self.duration, 83 | size=self.size, 84 | rate=self.rate, 85 | song_url=self.song_url, 86 | lyrics_url=self.lyrics_url, 87 | cover_url=self.cover_url, 88 | ) 89 | 90 | @property 91 | def available(self) -> bool: 92 | """ Not available when url is none or size equal 0 """ 93 | return bool(self.song_url and self.size) 94 | 95 | @property 96 | def name(self) -> str: 97 | """ Song file name """ 98 | return "%s - %s.%s" % (self.singer, self.title, self.ext) 99 | 100 | @property 101 | def duration(self): 102 | """ 持续时间 H:M:S """ 103 | return self._duration 104 | 105 | @duration.setter 106 | def duration(self, seconds): 107 | self._duration = str(datetime.timedelta(seconds=int(seconds))) 108 | 109 | @property 110 | def song_url(self) -> str: 111 | return self._song_url 112 | 113 | @song_url.setter 114 | def song_url(self, url): 115 | """ Set song url and update size. """ 116 | try: 117 | r = requests.get( 118 | url, 119 | stream=True, 120 | headers=config.get("wget_headers"), 121 | proxies=config.get("proxies"), 122 | ) 123 | self._song_url = url 124 | size = int(r.headers.get("Content-Length", 0)) 125 | # 转换成MB并保留两位小数 126 | self.size = round(size / 1048576, 2) 127 | # 设置完整的文件名(不含后缀) 128 | if not self._fullname: 129 | self._set_fullname() 130 | except Exception as e: 131 | self.logger.info(_("Request failed: {url}").format(url=url)) 132 | self.logger.info(e) 133 | 134 | @property 135 | def row(self) -> list: 136 | """ Song details in list form """ 137 | 138 | def highlight(s, k): 139 | return s.replace(k, colorize(k, "xiami")).replace( 140 | k.title(), colorize(k.title(), "xiami") 141 | ) 142 | 143 | ht_singer = self.singer if len(self.singer) < 30 else self.singer[:30] + "..." 144 | ht_title = self.title if len(self.title) < 30 else self.title[:30] + "..." 145 | ht_album = self.album if len(self.album) < 20 else self.album[:20] + "..." 146 | 147 | if config.get("keyword"): 148 | keywords = re.split(";|,|\s|\*", config.get("keyword")) 149 | for k in keywords: 150 | if not k: 151 | continue 152 | ht_singer = highlight(ht_singer, k) 153 | ht_title = highlight(ht_title, k) 154 | ht_album = highlight(ht_album, k) 155 | 156 | size = "%sMB" % self.size 157 | ht_size = size if int(self.size) < 8 else colorize(size, "flac") 158 | 159 | return [ 160 | colorize(self.idx, "baidu"), 161 | ht_title, 162 | ht_singer, 163 | ht_size, 164 | self.duration, 165 | ht_album, 166 | self.source.upper(), 167 | ] 168 | 169 | 170 | @property 171 | def title(self): 172 | return self._title 173 | 174 | @title.setter 175 | def title(self, value): 176 | value = re.sub(r'[\\/:*?"<>|]', "", value) 177 | self._title = value 178 | 179 | @property 180 | def singer(self): 181 | return self._singer 182 | 183 | @singer.setter 184 | def singer(self, value): 185 | value = re.sub(r'[\\/:*?"<>|]', "", value) 186 | self._singer = value 187 | 188 | def _set_fullname(self): 189 | """ Full name without suffix, to resolve file name conflicts""" 190 | outdir = config.get("outdir") 191 | outfile = os.path.abspath(os.path.join(outdir, self.name)) 192 | if os.path.exists(outfile): 193 | name, ext = self.name.rsplit(".", 1) 194 | names = [ 195 | x for x in os.listdir(outdir) if x.startswith(name) and x.endswith(ext) 196 | ] 197 | names = [x.rsplit(".", 1)[0] for x in names] 198 | suffixes = [x.replace(name, "") for x in names] 199 | # filter suffixes that match ' (x)' pattern 200 | suffixes = [ 201 | x[2:-1] for x in suffixes if x.startswith(" (") and x.endswith(")") 202 | ] 203 | indexes = [int(x) for x in suffixes if set(x) <= set("0123456789")] 204 | idx = 1 205 | if indexes: 206 | idx += sorted(indexes)[-1] 207 | self._fullname = os.path.abspath( 208 | os.path.join(outdir, "%s (%d)" % (name, idx)) 209 | ) 210 | else: 211 | self._fullname = outfile.rpartition(".")[0] 212 | 213 | @property 214 | def song_fullname(self): 215 | return self._fullname + "." + self.ext 216 | 217 | @property 218 | def lyrics_fullname(self): 219 | return self._fullname + ".lrc" 220 | 221 | @property 222 | def cover_fullname(self): 223 | return self._fullname + ".jpg" 224 | 225 | def _download_file(self, url, outfile, stream=False): 226 | """ 227 | Helper function for download 228 | :param url: 229 | :param outfile: 230 | :param stream: need process bar or not 231 | :return: 232 | """ 233 | if not url: 234 | self.logger.error("URL is empty.") 235 | return 236 | try: 237 | r = requests.get( 238 | url, 239 | stream=stream, 240 | headers=config.get("wget_headers"), 241 | proxies=config.get("proxies"), 242 | ) 243 | if stream: 244 | total_size = int(r.headers["content-length"]) 245 | with click.progressbar( 246 | length=total_size, label=_(" :: Downloading ...") 247 | ) as bar: 248 | with open(outfile, "wb") as f: 249 | for chunk in r.iter_content(chunk_size=1024): 250 | if chunk: 251 | f.write(chunk) 252 | bar.update(len(chunk)) 253 | else: 254 | with open(outfile, "wb") as f: 255 | f.write(r.content) 256 | click.echo( 257 | _(" :: Saved to: {outfile}").format( 258 | outfile=colorize(outfile, "highlight") 259 | ) 260 | ) 261 | except Exception as e: 262 | click.echo("") 263 | self.logger.error(_("Download failed: ") + "\n") 264 | self.logger.error(_("URL: {url}").format(url=url) + "\n") 265 | self.logger.error( 266 | _("File location: {outfile}").format(outfile=outfile) + "\n" 267 | ) 268 | if config.get("verbose"): 269 | self.logger.error(e) 270 | 271 | def _save_lyrics_text(self): 272 | with open(self.lyrics_fullname, "w", encoding="utf-8") as f: 273 | f.write(self.lyrics_text) 274 | click.echo( 275 | _(" :: Saved to: {outfile}").format( 276 | outfile=colorize(self.lyrics_fullname, "highlight") 277 | ) 278 | ) 279 | 280 | def download_song(self): 281 | if self.song_url: 282 | self._download_file(self.song_url, self.song_fullname, stream=True) 283 | else: 284 | self.logger.error(_("Download failed: song URL is empty")) 285 | 286 | def download_lyrics(self): 287 | if self.lyrics_url: 288 | self._download_file(self.lyrics_url, self.lyrics_fullname, stream=False) 289 | 290 | def download_cover(self): 291 | if self.cover_url: 292 | self._download_file(self.cover_url, self.cover_fullname, stream=False) 293 | 294 | def download(self): 295 | """ Main download function """ 296 | click.echo("===============================================================") 297 | if config.get("verbose"): 298 | click.echo(str(self)) 299 | else: 300 | click.echo(" | ".join(self.row)) 301 | 302 | self.download_song() 303 | if config.get("lyrics"): 304 | self.download_lyrics() 305 | if config.get("cover"): 306 | self.download_cover() 307 | 308 | click.echo("===============================================================\n") 309 | --------------------------------------------------------------------------------