不知從何時開始我們手機音樂app里的歌單,好多歌曲不是變灰了就是變成vip歌曲,普通用戶只能試聽幾十秒,作為一名白嫖黨怎么能忍,干脆擼起袖子自己動手把喜歡的歌曲全部保存下來。
- 咕米音樂網頁版:https://music.migu.cn/v3
說句題外話我覺得咕米音樂這個網站真的挺良心的,個人很喜歡,幾乎所有的音樂都可以無損播放。簡直不要太友好。不像某些大廠吃相太難看了,試聽只能普通音質,想要無損必須辦會員,那渣音質還不如不聽。關鍵是咕米音樂還能免費聽周董的歌【笑】
打開咕米音樂首頁,搜索自己喜歡的歌手進入他的主頁,如下圖:
點擊播放一首音樂,頁面進行跳轉,右鍵F12打開開發者模式重新刷新頁面,如圖所示:
我們分析請求信息得出如下鏈接即為歌曲請求的api
在response里便是服務端返回的歌曲信息
分析請求的參數,搜索參數名稱有如下結果,很容易就定位到加密參數的生成位置

打個斷點進行單步調試,變量e即為被加密的數據,經多番測試,其中copyrightId即歌曲的版權id;
type為音樂的音質類型,無損SQ type == 3,高質量HQ type == 2,普通音質 type == 1
auditionsFlag默認為0;變量n為固定值;變量 r、a即為我們提交的加密參數。
好了基本上分析完了,此處省略摳js代碼過程。

三、具體實現
"""
===================================
-*- coding:utf-8 -*-
Author :GadyPu
E_mail :Gadypy@gmail.com
Time :2020/8/25 0020 上午 12:08
FileName :migu_music.py
===================================
"""
import os
import requests
import warnings
from lxml import etree
from enum import Enum
from queue import Queue
import threading
warnings.filterwarnings('ignore')
class MusicType(Enum):
Normal = 1
HQ = 2
SQ = 3
class MiguMusic(object):
def __init__(self):
self.req_url = 'https://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?'
self.serach_api = 'https://music.migu.cn/v3/api/search/suggest?'
self.headers = {
'referer': 'https://music.migu.cn/v3/music/player/audio',
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36',
}
self.headers_song = {
'Host': 'freetyst.nf.migu.cn',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
}
self.params = {
'dataType': 2,
'data': None,
'secKey': None
}
self.que = Queue()
self.song_list = []
def get_music_download_url(self, song_id, type = 3):
'''
獲取歌曲下載鏈接
:param song_id: 歌曲copyrightId
:param type: 默認下載無損格式的音頻
:return:
'''
try:
secKey = os.popen('Node fuck.js ' + f'{song_id} {type}').read().split(' ')
except:
print('error cannot create secKey')
return None
self.params['data'] = secKey[0]
self.params['secKey'] = secKey[1]
try:
response = requests.get(url = self.req_url, headers = self.headers, params = self.params, verify = False)
print(response.text)
if response.status_code == 200:
url = 'http:' + response.json()['data']['playUrl']
return url
except:
print('network error please try again...')
return None
def get_singer_or_music_id(self, keyword, type):
'''
獲取歌手id或者歌曲id
:param keyword:
:param type: type == 1 keyword為歌手id,反之為歌曲id
:return:
'''
param = { 'keyword': keyword }
headers = {
'referer': 'https://music.migu.cn/v3?keyword=',
'Host': 'music.migu.cn',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
}
try:
response = requests.get(url = self.serach_api, headers = headers, params =param, verify = False)
data = response.json()
return data['data']['singers'][0]['id'] if type == 1 else data['data']['songs'][0]['copyrightId']
except:
return None
def get_song_list(self, keyword, page = 1, type = 1):
'''
獲取歌手主頁歌曲信息
eg: https://music.migu.cn/v3/music/artist/559/song
:param keyword: 歌手名稱或歌曲名稱
:param page: 歌手主頁,頁碼
:param type: type == 1,keyword為歌手名稱、type == 2, keyword為歌曲名稱
:return: None
'''
id = self.get_singer_or_music_id(keyword, type)
if not id or type == 2:
if id:
self.song_list.append([id, keyword, MusicType['SQ'].value])
return None
songs_api = 'https://music.migu.cn/v3/music/artist/{id}/song?page={page}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
}
url = songs_api.format(
id = id, page = page
)
try:
#歌手主頁,獲取歌曲:id、名稱、音質
response = requests.get(url = url, headers = headers, verify = False)
html = etree.HTML(response.text)
ids = html.xpath('//div[@class="song-name J_SongName"]/a/@href')
titles = html.xpath('//div[@class="song-name J_SongName"]/a/@title')
types = html.xpath('//div[@class="song-name J_SongName"]/i/text()')
for _id, tit, typ in zip(ids, titles, types):
try:
__typ = MusicType[typ.upper()].value
except:
__typ = MusicType['Normal'].value
finally:
self.song_list.append([_id.split('/')[-1], tit, __typ])
#print(_id, tit, typ)
except Exception as err:
print(err)
def download_thread(self, path):
'''
下載線程
:param path: 下載保存的路徑
:return:
'''
while not self.que.empty():
__d = self.que.get()
try:
resp = requests.get(url = __d[0], headers = self.headers_song, stream = True)
if resp.status_code == 200:
print(f'start downloading {__d[1]}')
file_path = os.path.join(path, f'{__d[1]}.flac')
with open(file_path, 'wb') as wf:
for data in resp.iter_content(1024):
if data:
wf.write(data)
print(f'the file {__d[1]} download complished...')
except:
print('download network error...')
def run(self, keyword, page = 1, type = 1):
'''
程序入口
:param keyword: 歌手名稱或歌曲名稱
:param page: 默認下載歌手主頁第一頁的所有歌曲
:param type: type == 1,keyword為歌手名稱、type == 2, keyword為歌曲名稱
:return:
'''
self.get_song_list(keyword, page, type)
if not self.song_list:
print('song list empty...')
return
for i in self.song_list:
if i and i[0]:
self.que.put((self.get_music_download_url(i[0], i[2]), i[1]))
# keyword為歌手,創建歌手文件夾,否則保存路徑為當前目錄下的music文件夾
path = os.path.join(os.getcwd(), keyword if type == 1 else 'music')
if not os.path.exists(path):
os.makedirs(path)
thread_list = []
for i in range(3):
t = threading.Thread(target = self.download_thread, args = (path, ))
t.setDaemon(True)
thread_list.append(t)
t.start()
[_.join() for _ in thread_list]
if __name__ == '__main__':
d = MiguMusic()
#d.run('許巍') #用歌手名下載
d.run('曾經的你', 1, 2)
由於js代碼較長約莫2w余行,此處就不上傳了,如果有需要的小伙伴評論區留言或者私信我也行。
以上代碼僅供交流學習之用,切勿用作其它用途。
部分js代碼:


