再見二丁目 | yitimo的个人博客

再见二丁目

使用golang调用网易云音乐api

发布于: 2017-11-06 21:07

此文章久未修订,请自行甄别内容准确性。

2018-01-26 迁移更新

现已将所有已实现api都使用新版加密请求方式,部分调用url及参数参考自 NeteaseCloudMusicApi


本文将展示笔者经过肤浅的golang学习后,使用golang对网易云音乐api进行分析的经历,最终实现一个基本的api,可以做到代为向网易云音乐的api请求数据。笔者使用的是martini web框架,并参考了网上现有的各种网易云音乐api的python实现,在此一并谢过。 目前实现的api只有以下几个:

  1. 歌曲搜索
  2. 歌曲详情
  3. 含实际mp3地址的下载信息

其中第三个下载能力是最磨人的,不过毕竟是可以直接获取下载链接的接口,还算值得。

一、搜索能力

这里使用的是 http://music.163.com/api/search/get/ 这个api地址,发送的是POST请求,需要注意的点就是必须带上的几个头部以及参数的格式,实现难度相对中等。在golang下的请求核心代码如下:

/** 
 * 执行搜索
 * params: 	关键词 类型 页码 数量
 * return:	字符串形式的请求结果
 */
func Search(words string, stype string, page int, limit int) string {
	// 创建客户端
	client := &http.Client{}
	// 格式化参数
	_o, _l := formatParams(page, limit)
	// 设置body
	form := url.Values{}
	form.Set("s", words)
	form.Set("type", stype)
	form.Set("limit", _l)
	form.Set("offset", _o)
	body := strings.NewReader(form.Encode())
	// 创建请求
	request, _ := http.NewRequest("POST", "http://music.163.com/api/search/get/", body)
	//设置头部
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	request.Header.Set("Cookie", "appver=2.0.2")
	request.Header.Set("Referer", "http://music.163.com")
	request.Header.Set("Content-Length", (string)(body.Len()))
	// 发起请求
	response, reqErr := client.Do(request)
	// 错误处理
	if reqErr!= nil {
		fmt.Println("Fatal error ", reqErr.Error())
		return `{"data": null, "state": false, "msg": "请求失败"}`
	}
	defer response.Body.Close()
	resBody, _ := ioutil.ReadAll(response.Body)
	return string(resBody)
}

/**
* 传入 搜索类型 页码 数量
* 返回 搜索类型 偏移 数量
*/
func formatParams(page int, limit int) (string, string) {
	if page < 1 {
		page = 1
	}
	if limit < 1 {
		limit = 0
	}
	return strconv.Itoa((page - 1) * limit), strconv.Itoa(limit)
}

传入的参数即 关键词,搜索类型,数量,偏移量 四个。最终效果像这样:

请求参数

请求结果

二、歌曲详情

歌曲详情接口比搜索接口要简单很多,因为是更开放的GET请求,不需要加复杂的头部:

func SongInfo(id string) string {
	res, err := http.Get("http://music.163.com/api/song/detail/?id=" + id + "&ids=[" + id + "]")
	// 错误处理
	if err != nil {
		fmt.Println("Fatal error ", err)
		return `{code: 0}`
	}
	defer res.Body.Close()
	rs, _ := ioutil.ReadAll(res.Body)
	return string(rs)
}

请求结果像这样:

请求结果

相比搜索接口的结果,详情接口能拿到具体的封面信息这应该是最有用的,因为搜索得到的结果中图片都是假的,可惜的是详情结果中的 mp3Url 字段是空的,说明想要真正听到歌曲还得使用别的接口才行。

三、歌曲下载

歌曲下载接口应该是变动最多的接口了,经测试很多网上现有的方式都没有用,最靠谱的应该是使用其官方的请求方式,接口为 http://music.163.com/weapi/song/enhance/player/url?csrf_token= ,参数为两个很长的加密的字符串,要做的斗争就是如何得到这两个字符串。

官网中的请求

参考网上现有的前辈大佬的分析,个人描述的加密过程如下:

  1. 将请求参数使用一个固定的标识字符串进行 AES加密并进行base64编码
  2. abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/ 中随机取出16次字符组成一个秘钥
  3. 将1得到的结果使用2得到的秘钥串再进行一次 AES加密并base64编码,至此得到第一个参数 params
  4. 将2中的秘钥倒序后转ascii码,然后转16进制字符串得到一个中间字符串
  5. 将4得到的中间字符串转为十进制大整数1,将另两个固定的16进制字符串也都转为十进制大整数2、3
  6. 5中得到了三个大整数,现在执行 pow(大整数1, 大整数2, 大整数3) ,即1的2次幂取模3,得到新的大整数4
  7. 将6得到的大整数4转为16进制字符串,并在左边补满0补满256位,最终得到第二个参数 encSecKey

封装好的函数如下:

func EncParams(param string) (string, string, error) {
	// 创建 key
	secKey := createSecretKey(16);
	// 第一次加密 使用固定的 nonce
	if aes1, err1 := aesEncrypt(param, nonce); err1 != nil {
		return "", "", err1
	} else {
		// 第二次加密 使用创建的 key
		if aes2, err2 := aesEncrypt(aes1, secKey); err2 != nil {
			return "", "", err2
		} else {
			// 得到 加密好的 param 以及 加密好的key
			return aes2, rsaEncrypt(secKey, pubKey, modulus), nil
		}
	}
}

具体的实现有个小几十行,详见 github 吧。

得到两个参数后,发的是POST请求,顺利得到结果:

请求结果

收尾总结

第三个接口中对笔者来说磨人的两点在于:

  1. 对golang经验不够导致数据类型转来转去花了不少力气,以及还涉及到了大数运算,数据的格式化真的是虐惨了
  2. 上文提到的 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/ 这个字符串中, 最后的 +/ 很关键不能少