前言

上次的项目 LRCEditor 告一段落之后,最近又想到对其进行优化,其实也就是添加一个常用的功能:在线搜索歌词

LRCEditor 中的 “帮助” 选项里面有提到如何在网易云音乐的在线界面利用开发者模式获取歌词文件和翻译,但是此方法操作起来会有一定难度,不利于将来准备做的 MusicPlayer 的功能集成,因此想到通过程序自己访问网络获取歌词并显示

在我们平常浏览的播放器网页里面,通常都可以看到歌词,歌曲名称,作者信息和封面信息等等,然而要让程序从网页中像人一样提取出这些信息无疑是非常难的,如何让这些信息以程序能够理解或者解析的格式直接呈现呢,这就要依网页API来实现了

实现(获取歌词)

Step1

使用网易云提供的 API,根据给定的歌曲名称(设为 Faded)搜索相应信息:http://music.163.com/api/search/get?s=Faded&limit=20&type=1&offset=0 ,必需参数说明:

  • http://music.163.com/api/search/get API接口网址
  • s:歌曲名称,中文要进行字符编码
  • limit:返回结果的数量
  • type:搜索类型,1为单曲,10为专辑,100为歌手,1000歌单,1002用户
  • offset:偏移量,用于结果分页返回

在浏览器地址栏输入这个网址后,可以看到返回了一坨信息,大多都是如下格式的 JSON 代码:

1
{"id":36990266,"name":"Faded","artists":[{"id":1045123,"name":"Alan Walker","picUrl":null,"alias":[],"albumSize":0,"picId":0,"img1v1Url":"http://p2.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1":0,"trans":null},{"id":1078390,"name":"Iselin Solheim","picUrl":null,"alias":[],"albumSize":0,"picId":0,"img1v1Url":"http://p2.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1":0,"trans":null}],"album":{"id":3406843,"name":"Faded","artist":{"id":0,"name":"","picUrl":null,"alias":[],"albumSize":0,"picId":0,"img1v1Url":"http://p2.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1":0,"trans":null},"publishTime":1448380800007,"size":4,"copyrightId":7001,"status":3,"picId":18277181788626198,"mark":0},"duration":212626,"copyrightId":7001,"status":0,"alias":[],"rtype":0,"ftype":0,"mvid":524116,"fee":8,"rUrl":null,"mark":8192}

其中,第一个 "id" 就是该歌曲在网易云音乐的编号,过后的信息如英文所示,"name" 表示歌曲名,"artists" 中列出了参与创作的艺术家名称(因为可能不止一个,所以是以数组的形式返回的),"duration" 表示歌曲持续的时间(单位为毫秒),这一步的作用只是通过API搜索出歌曲ID,下一步将再次通过API得到ID对应歌曲的歌词信息

Step2

使用此网址得到歌曲的歌词信息:http://music.163.com/api/song/lyric?os=pc&id=36990266&lv=-1&kv=-1&tv=-1

地址中除了 id= 后面接上上一步得到的 ID 外,其他部分不用管,然后可以看到以下 JSON 信息:

1
{"sgc":false,"sfy":false,"qfy":false,"transUser":{"id":36990266,"status":99,"demand":1,"userid":1569973972,"nickname":"……ye","uptime":1548668567698},"lyricUser":{"id":36990266,"status":99,"demand":0,"userid":1569973972,"nickname":"……ye","uptime":1547805089718},"lrc":{"version":16,"lyric":"……"},"klyric":{"version":0,"lyric":null},"tlyric":{"version":10,"lyric":"……"},"code":200}

详细的歌词部分用 “……” 代替了,返回信息中 lrc 是原版歌词,若歌曲非中文且网易云有翻译版,则 tlyric 为最新一的翻译版歌词,nicknameuserid 是翻译贡献者的用户名和用户ID

到这一步,我们就已经取得了关于歌词的较为完整的信息,但是目前为止,上述步骤都是都是在浏览器中完成的,要让程序做到够像在浏览器地址栏中输入网址然后获取返回信息的话,需要额外的代码

并且,返回字符串也并不是原原本本的歌词,而是以 JSON 格式编码的返回信息,为了处理 JSON 格式的返回信息,需要用到 Newtonsoft.JSON

Step3

以下代码用于从给定网址读取信息,以 String 格式返回,原理不多做解释了

1
2
3
4
5
6
7
8
9
10
11
12
13
Imports System.Security.Cryptography.X509Certificates
Imports System.Net
Public Function GetInfo(ByVal url As String) As String
Dim ret As String = ""
Dim HttpUrl As New Uri(url)
Dim HttpReq As HttpWebRequest = WebRequest.Create(HttpUrl)
Dim HttpRep As HttpWebResponse = HttpReq.GetResponse()
Dim resStream As IO.Stream = HttpRep.GetResponseStream()
Dim sr As New IO.StreamReader(resStream, System.Text.Encoding.UTF8)
ret = sr.ReadToEnd()
sr.Close()
Return ret
End Function

调用例子:

1
2
Dim ResPUrl As String = "http://music.163.com/api/search/get?&limit=20&type=1&offset=0&s=" + Given 'Given 给定歌曲的名称
Dim WebRet = GetInfo(ResPUrl)

此时,WebRet 存储的就是 Step1 里面看到的一大坨东西了,接下来使用 Newtonsoft.JSON 框架里的函数解析这个字符串,获得我们想要的歌曲信息

Step4

先从 NuGet 导入 Newtonsoft 的包,然后 Imports Newtonsoft.JSON

这一步使用 JsonConvert.DeserializeObject 对字符串进行反序列化并存储对应标签的键值

先看一下函数声明:JsonConvert.DeserializeObject(Of T As Type)(Value As String) As Object,是一个很奇怪的函数,可以把它和 C++ 里面的 Template<Typename T> 类比,即前面一个括号里面写的 (Of T As Type) 限定此函数返回的类型,后面一个括号里面是我们要进行反序列化操作的 JSON 字符串

JsonConvert.DeserializeObject 函数本身返回的是 Object 类型,这是 VB.NET 里面的最基本类型,可以派生出任意的类型,拥有他们的结构,但是 Object 本身不具有任何特定的成员变量或者函数,需要将特定的结构体传入到 Object 才能让它像其他类或者结构体一样使用

JsonConvert.DeserializeObject 函数返回的是一个特定的类或者结构体,其组成和要操作的 JSON 字符串一致,即拥有相同的结构和变量名,例如以下 JSON 字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Competitors": [
{
"name": "HQX",
"score": 400,
"time": "0ms"
},
{
"name": "WYX",
"score": 0,
"time": "1000ms"
}
]
}

从此字符串返回的特定结构体或类应具有如下组成形式,才能让函数正确返回:

1
2
3
4
5
6
7
8
Public Structure JRES
Public Structure JRES_PERSON
Public name,time As String
Public score As Integer
End Structure
Public Competitors As List(Of JRES_PERSON)
'若 JSON 返回的值不带引号,需要用Integer或Long类型接收
End Structure

需要说明的是,特定结构体不一定要有全部 JSON 里面的变量名称,但是不能多出 JSON 里没有的变量名称,例如删去成员 name ,函数仍然正确返回,但是若加上成员 age 就会出现错误(不管是什么类型),对于 JSON 返回值中带方括号声明的数组类型,需要用同名且拥有一致的结构的集合类型进行接收,如 List 或 数组

现在,分析一下 Step1 中JSON返回值的结构:

pic

整个结构非常的繁琐而且层次很深,因此我们只提取想要的几个信息,可以用如下的特定结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Public Structure JRES
Public code As Integer
Public Structure JRES_SONGS
Public songCount As Integer
Public Structure JRES_SINGLESONG
Public id, duration As Integer
Public name As String
Public Structure JRES_SINGLESONG_ARTISTS
Public name As String
End Structure
Public artists As List(Of JRES_SINGLESONG_ARTISTS)
End Structure
Public songs As List(Of JRES_SINGLESONG)
End Structure
Public result As JRES_SONGS
End Structure

然后:

1
2
3
Public Res As JRES
Res = JsonConvert.DeserializeObject(Of JRES)(TarGetResp)
'TarGetResp 是上一步取得的网页返回值

现在,结构体 Res 里面存储的就是我们希望得到的歌词信息了,如果用户还限制了艺术家名称,我们可以将不符合的项从 List 当中去掉,然后选取一个控件呈现得到的信息吧!

实现(获取专辑封面)

Step 1

同样是网易云的 API 网址:http://music.163.com/api/song/detail/?id={歌曲id}&ids=[{歌曲id}]

获取到如下返回值:

1
{"songs":[{"name":"Umbrella (Matte Remix)","id":518904426,"position":1,"alias":[],"status":0,"fee":0,"copyrightId":0,"disc":"01","no":1,"artists":[{"name":"Matte","id":12335355,"picId":0,"img1v1Id":0,"briefDesc":"","picUrl":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1Url":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","albumSize":0,"alias":[],"trans":"","musicSize":0,"topicPerson":0},{"name":"Ember Island","id":1100001,"picId":0,"img1v1Id":0,"briefDesc":"","picUrl":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1Url":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","albumSize":0,"alias":[],"trans":"","musicSize":0,"topicPerson":0}],"album":{"name":"Umbrella (Matte Remix)","id":36812124,"type":"EP/Single","size":1,"picId":109951163063843501,"blurPicUrl":"http://p1.music.126.net/1LrtvH8EpKb5iHKR9qEU0Q==/109951163063843501.jpg","companyId":0,"pic":109951163063843501,"picUrl":"http://p1.music.126.net/1LrtvH8EpKb5iHKR9qEU0Q==/109951163063843501.jpg","publishTime":1510502400007,"description":"","tags":"","company":"Self-Release","briefDesc":"","artist":{"name":"","id":0,"picId":0,"img1v1Id":0,"briefDesc":"","picUrl":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1Url":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","albumSize":0,"alias":[],"trans":"","musicSize":0,"topicPerson":0},"songs":[],"alias":[],"status":0,"copyrightId":0,"commentThreadId":"R_AL_3_36812124","artists":[{"name":"Matte","id":12335355,"picId":0,"img1v1Id":0,"briefDesc":"","picUrl":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","img1v1Url":"http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg","albumSize":0,"alias":[],"trans":"","musicSize":0,"topicPerson":0}],"subType":"混音版","transName":null,"mark":0,"picId_str":"109951163063843501"},"starred":false,"popularity":100.0,"score":100,"starredNum":0,"duration":285701,"playedNum":0,"dayPlays":0,"hearTime":0,"ringtone":null,"crbt":null,"audition":null,"copyFrom":"","commentThreadId":"R_SO_4_518904426","rtUrl":null,"ftype":0,"rtUrls":[],"copyright":0,"transName":null,"sign":null,"mark":0,"hMusic":{"name":null,"id":1387911675,"size":11430182,"extension":"mp3","sr":44100,"dfsId":0,"bitrate":320000,"playTime":285701,"volumeDelta":-19400.0},"mMusic":{"name":null,"id":1387911676,"size":6858127,"extension":"mp3","sr":44100,"dfsId":0,"bitrate":192000,"playTime":285701,"volumeDelta":-16600.0},"lMusic":{"name":null,"id":1387911677,"size":4572099,"extension":"mp3","sr":44100,"dfsId":0,"bitrate":128000,"playTime":285701,"volumeDelta":-14700.0},"bMusic":{"name":null,"id":1387911677,"size":4572099,"extension":"mp3","sr":44100,"dfsId":0,"bitrate":128000,"playTime":285701,"volumeDelta":-14700.0},"mvid":0,"rtype":0,"rurl":null,"mp3Url":null}],"equalizers":{},"code":200}

这个 JSON 返回值的结构和上面提到的获取歌词时候的返回值结构相似,但是没有最外层的 result ,所以还是需要再写一个结构体接收,如果用上述获取歌词的结构体接收,其 songs 会为 nothing

成功接收数据后,songs(0).album.picurl 就是专辑封面图片的网址

Step 2

得到网址后,注意到这次的网址返回的不是纯字符串而是文件,所以不能再用像获取歌词的 GetResponse 的方法得到图片了,此时我们可以用 .NET 自带的 HTTP 下载器 WebClint 完成操作

WebClint 的下载操作都是默认异步执行的,不会造成主窗体死锁的现象,但是 WebClint 不在控件箱里,不能可视化对其触发事件进行函数绑定,因此我们用 AddHandler 手动绑定几个函数,首先,请求下载的操作为:

1
2
Dim wc As WebClint
wc.DownloadFileAsync(TargetUrl, TargetFileName)

其中 TargetFileName 是下载文件保存到本地时的文件名,然后我们给两个触发事件绑定函数

1
2
AddHandler wc.DownloadProgressChanged, AddressOf ShowDownProcess
AddHandler wc.DownloadFileCompleted, AddressOf DownloadComplete

其中 DownloadProcessChanged 在下载进度改变时触发,DownloadFileCompleted 在文件下载完成后触发,在对应的函数 ShowDownProcess 里可以使用 ProcessBar 控件直观显示下载进度,下载完成后 Process.Start 启动文件

1
2
3
4
5
6
7
8
Public Sub ShowDownProcess(ByVal sender As Object, ByVal e As System.Net.DownloadProgressChangedEventArgs)
pb1.Value = e.ProgressPercentage
Lpro.Text = "已完成:" + CStr(e.ProgressPercentage) + " / " + "100"
End Sub
Public Sub DownloadComplete(ByVal sender As Object, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)
Process.Start(TargetFileName)
Me.Close()
End Sub

这一步可以单独用一个窗体来解决,使得程序更直观

实现(获取mp3文件)

Step1

API网址为:http://music.163.com/song/media/outer/url?id={歌曲id}

打开这个网址会得到一个可以直接下载的 mp3 文件,然后直接使用上面提到的 WebClint 下载即可

附:窗体代码(下载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Option Explicit On
Imports System.Security.Cryptography.X509Certificates
Imports Newtonsoft.Json
Imports System.Net
Imports System.Diagnostics
Public Class Donw
Public wc As WebClient
Public TargetUrl As Uri
Public TargetFileName As String
Public Sub callDialog(songname As String, songart As String, songid As Integer)
TargetUrl = New Uri("http://music.163.com/song/media/outer/url?id=" + CStr(songid) + ".mp3")
TargetFileName = songname + " - " + songart + ".mp3"
Ltitle.Text = "下载歌词:" + TargetFileName
pb1.Value = 0
wc = New WebClient()
AddHandler wc.DownloadProgressChanged, AddressOf ShowDownProcess
AddHandler wc.DownloadFileCompleted, AddressOf DownloadComplete
Me.ShowDialog()
End Sub
Public Sub callDialog_PIC(songname As String, songart As String, picurl As String)
TargetUrl = New Uri(picurl)
TargetFileName = songname + " - " + songart + ".png"
Ltitle.Text = "下载专辑图片:" + TargetFileName
pb1.Value = 0
wc = New WebClient()
AddHandler wc.DownloadProgressChanged, AddressOf ShowDownProcess
AddHandler wc.DownloadFileCompleted, AddressOf DownloadComplete
Me.ShowDialog()
End Sub
Public Sub ShowDownProcess(ByVal sender As Object, ByVal e As System.Net.DownloadProgressChangedEventArgs)
pb1.Value = e.ProgressPercentage
Lpro.Text = "已完成:" + CStr(e.ProgressPercentage) + " / " + "100"
End Sub
Public Sub DownloadComplete(ByVal sender As Object, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)
Process.Start(TargetFileName)
Me.Close()
End Sub

Private Sub Donw_Load(sender As Object, e As EventArgs) Handles MyBase.Load
wc.DownloadFileAsync(TargetUrl, TargetFileName)
End Sub

Private Sub Donw_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
If Not wc.IsBusy Then wc.CancelAsync()
End Sub
End Class