├── .gitignore ├── LICENSE ├── README.MD ├── _config.yml ├── auth.py ├── requirements.txt ├── run.py ├── search.py ├── songs.py ├── songs └── PUT_SONGS_HERE └── update.py /.gitignore: -------------------------------------------------------------------------------- 1 | songs/*.mp3 2 | songs/*.jpg 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Reynard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | # [Deprecated] 3 | 4 | 5 | # WHAT'S THIS? 6 | A project aimng at simplify embedding albumn cover for songs downloaded from XIAMI MUSIC or QQ MUSIC, which do not contain full ID3 informations. 7 | 8 | # COVERS FROM? 9 | For now, all the covers are searched, matched and downloaded from NETEASE MUSIC, coz the author is justing doing something related which provided a kind of convenience. 10 | 11 | # HOW TO USE(Tested on Ubuntu 17.04)? 12 | -- `git clone` this repo 13 | -- Open a new terminal windows in the repo, install requirements using `pip install -Ur requirements.txt` 14 | -- Cpoy all your target songs(only mp3 supported) to the folder `songs` 15 | -- run `python3 run.py` 16 | SPEED UP: Configure your NETEASEMUSIC account at `LINE 17` in auth.py to avoid servers' anti-crawl strategies. 17 | 18 | # NOTICE 19 | -- For now, songs are searched by FILE NAME (could be easily modified by pressing F2) 20 | -- ONLY support mp3 21 | -- ONLY fetch album covers from NETEASE MUSIC 22 | 23 | # KNOWN ISSUES 24 | -- Doesn't support QQMUSIC files' naming pattern 25 | -- Songs not in NETEASEMUSIC library might be matched with wrong cover 26 | -- Might not work on Windows(due to the installation of dependency? not tested) 27 | 28 | # TODO 29 | Nope,really... 30 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | import requests 5 | import json 6 | import re 7 | import time 8 | import os 9 | import base64 10 | import binascii 11 | from bs4 import BeautifulSoup 12 | from Crypto.Cipher import AES 13 | from pprint import pprint 14 | 15 | # 验证信息 16 | text = { 17 | 'username': '', 18 | 'password': '', 19 | 'rememberLogin': 'true' 20 | } 21 | 22 | # def loadName(): 23 | # global BASE_URL, headers 24 | BASE_URL = 'http://music.163.com' 25 | # 定义请求头 26 | headers = { 27 | 'Refer': 'http://music.163.com', 28 | 'Host': 'music.163.com', 29 | 'User-Agent': 'android', 30 | 'Accept-Encoding': 'gzip', 31 | 'Cookie': 'appver=4.1.3; os=android; osver=7.1.1; mobilename=ONEPLUS3010; resolution=1920x1080; channle=netease', 32 | } 33 | 34 | # Default_Header = { 35 | # 'Referer': 'http://music.163.com/', 36 | # 'Host': 'music.163.com', 37 | # 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0 Iceweasel/38.3.0', 38 | # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 39 | # 'Accept-Encoding': 'gzip, deflate' 40 | # } 41 | 42 | 43 | # 加密信息 44 | modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 45 | nonce = '0CoJUm6Qyw8W8jud' 46 | pubKey = '010001' 47 | 48 | # 函数部分 49 | 50 | 51 | def aesEncrypt(text, secKey): 52 | pad = 16 - len(text) % 16 53 | text = text + pad * chr(pad) 54 | encryptor = AES.new(secKey, 2, '0102030405060708') 55 | ciphertext = encryptor.encrypt(text) 56 | ciphertext = base64.b64encode(ciphertext) 57 | return ciphertext.decode() 58 | 59 | 60 | def rsaEncrypt(text, pubKey, modulus): 61 | text = text[::-1].encode() 62 | rs = int(binascii.hexlify(text), 16)**int(pubKey, 16) % int(modulus, 16) 63 | return format(rs, 'x').zfill(256) 64 | 65 | 66 | def createSecretKey(size): 67 | return ''.join([hex(x)[2:] for x in os.urandom(size)])[0:16] 68 | 69 | 70 | text = json.dumps(text) 71 | secKey = createSecretKey(16) 72 | encText = aesEncrypt(aesEncrypt(text, nonce), secKey) 73 | encSecKey = rsaEncrypt(secKey, pubKey, modulus) 74 | data = { 75 | 'params': encText, 76 | 'encSecKey': encSecKey 77 | } 78 | 79 | _session = requests.session() 80 | _session.headers.update(headers) 81 | 82 | 83 | if __name__ == '__main__': 84 | 85 | # TESTING!!!!!!!!!!!!111 86 | print('TESTING!!!!!!!!!!!!!!!!!!!!!!') 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.20.0 2 | pycrypto==2.6.1 3 | Pillow==6.2.0 4 | beautifulsoup4==4.6.0 5 | mutagen==1.38 6 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | import songs 5 | import search 6 | import update 7 | 8 | 9 | def main(dir='./songs'): 10 | file_list = songs.list_dir(dir) 11 | for i in file_list: 12 | url = search.get_cover(i) 13 | file_path = './songs/' + str(i) + '.mp3' 14 | cover_path = file_path + '.jpg' 15 | try: 16 | # print(file_path) 17 | # update.update_cover(file_path, url) 18 | update.download_cover(file_path, url) 19 | update.resize_img(cover_path) 20 | update.add_metadata_to_song(file_path, cover_path) 21 | print(str(i), 'Success!') 22 | except Exception as e: 23 | print(e) 24 | 25 | if __name__ == '__main__': 26 | try: 27 | main() 28 | finally: 29 | print('\nDone!') 30 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf8 -*- 3 | import requests 4 | import auth 5 | import json 6 | 7 | 8 | def replace_name(music_name): 9 | music_name = music_name.split('[')[0].split( 10 | '(')[0].replace(' - ', ' ').replace('_', ' ') 11 | # print(music_name) 12 | return(music_name) 13 | 14 | 15 | def get_cover(music_name): 16 | music_name = replace_name(music_name) 17 | url = auth.BASE_URL + '/api/search/get' 18 | data = { 19 | 's': music_name, 20 | 'type': 10, 21 | 'offset': 0, 22 | 'total': 'true', 23 | # 'limit': limit 24 | } 25 | req = requests.post(url, headers=auth.headers, data=data) 26 | cover_url = req.json()['result']['albums'][0]['blurPicUrl'] 27 | cover_url.replace(' ', '') 28 | # print(cover_url) 29 | return(cover_url) 30 | 31 | 32 | if __name__ == '__main__': 33 | 34 | music_name = 'something just like this' 35 | print(get_cover(music_name)) 36 | 37 | # TESTING!!!!!!!!!!!!111 38 | print('TESTING!!!!!!!!!!!!!!!!!!!!!!') 39 | -------------------------------------------------------------------------------- /songs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def list_dir(path="./songs"): 5 | 6 | file_list = [] 7 | files = os.listdir(path) # 得到文件夹下的所有文件名称 8 | for file in files: # 遍历文件夹 9 | if not os.path.isdir(file): # 判断是否是文件夹,不是文件夹才打开 10 | if os.path.splitext(file)[1] == '.mp3': 11 | file_name = os.path.splitext(file)[0] # filenames 12 | # file_name = os.path.join(path, file) #filepaths&filenames 13 | file_list.append(file_name) 14 | else: 15 | pass 16 | print(len(file_list), 'songs founded, they are: ', file_list, '\n') 17 | return(file_list) 18 | 19 | if __name__ == '__main__': 20 | list_dir() 21 | 22 | # TESTING!!!!!!!!!!!!111 23 | print('TESTING!!!!!!!!!!!!!!!!!!!!!!') 24 | -------------------------------------------------------------------------------- /songs/PUT_SONGS_HERE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reycn/cover4mp3/ebab8e07b59ccca6a24a6f70084cf8dd57b5868b/songs/PUT_SONGS_HERE -------------------------------------------------------------------------------- /update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | import requests 7 | 8 | 9 | from mutagen.mp3 import MP3, HeaderNotFoundError 10 | from mutagen.id3 import ID3, APIC, TPE1, TIT2, TALB, error 11 | from PIL import Image 12 | 13 | 14 | def resize_img(file_path, max_size=(640, 640), quality=90): 15 | try: 16 | img = Image.open(file_path) 17 | except IOError: 18 | print('Can\'t open image:', file_path) 19 | return 20 | 21 | if img.size[0] > max_size[0] or img.size[1] > max_size[1]: 22 | img.thumbnail(max_size, Image.ANTIALIAS) 23 | img.save(file_path, quality=quality) 24 | 25 | # def update_cover(mp3_name, pic_url): 26 | 27 | 28 | def add_metadata_to_song(file_path, cover_path): 29 | # If no ID3 tags in mp3 file 30 | try: 31 | audio = MP3(file_path, ID3=ID3) 32 | except HeaderNotFoundError: 33 | print('Can\'t sync to MPEG frame, not an validate MP3 file!') 34 | return 35 | 36 | if audio.tags is None: 37 | print('No ID3 tag, trying to add one!') 38 | try: 39 | audio.add_tags() 40 | audio.save() 41 | except error as e: 42 | print('Error occur when add tags:', str(e)) 43 | return 44 | 45 | # Modify ID3 tags 46 | id3 = ID3(file_path) 47 | # Remove old 'APIC' frame 48 | # Because two 'APIC' may exist together with the different description 49 | # For more information visit: 50 | # http://mutagen.readthedocs.io/en/latest/user/id3.html 51 | if id3.getall('APIC'): 52 | id3.delall('APIC') 53 | # add album cover 54 | id3.add( 55 | APIC( 56 | # 3 is for UTF8, but here we use 0 (LATIN1) for 163, orz~~~ 57 | encoding=0, 58 | mime='image/jpeg', # image/jpeg or image/png 59 | type=3, # 3 is for the cover(front) image 60 | data=open(cover_path, 'rb').read() 61 | ) 62 | ) 63 | id3.save(v2_version=3) 64 | 65 | 66 | def download_cover(mp3_name, pic_url): 67 | # edit the ID3 tag to add the title, artist, artwork, date, and genre 68 | req = requests.get(pic_url) 69 | # imagedata = Image.open(BytesIO(req.content)) 70 | # imagedata = open('a.jpg', 'rb').read() 71 | # print(im) 72 | f = open(mp3_name+'.jpg', 'wb') 73 | f.write(requests.get(pic_url).content) 74 | f.close() 75 | # print(mp3_name+'.jpg') 76 | return(mp3_name+'.jpg') 77 | 78 | if __name__ == '__main__': 79 | # TESTING!!!!!!!!!!!!111 80 | print('TESTING!!!!!!!!!!!!!!!!!!!!!!') 81 | dir_path = './songs/' 82 | mp3_name = '凡人歌_李宗盛' 83 | mp3_path = dir_path + mp3_name + '.mp3' 84 | 85 | download_cover(mp3_path, 86 | 'http://p3.music.126.net/ggnyubDdMxrhpqYvpZbhEQ==/3302932937412681.jpg') 87 | resize_img(mp3_path + '.jpg') 88 | add_metadata_to_song(mp3_path, mp3_path + '.jpg') 89 | --------------------------------------------------------------------------------