新闻详细
新闻当前位置:新闻详细

nodejs怎么免费版网易云听歌,开源的网易云音乐API项目都是怎么实现的?

专业自媒体运营推广——顾家有收入两不误

电话+V: 152079-09430 ,欢迎咨询网易云音乐的免费音乐 API,[专业自媒体运营推广],[自媒体商圈业内交流],[各种运营推广课程],[解决从零到一的问题],[让你站在风口忘记焦虑]

一、nodejs怎么免费版网易云听歌

nodejs免费版网易云听歌步骤如下。

1、安装Node.js,并确保已经配置好环境变量。

2、在命令行工具中执行以下命令,安装网易云音乐Node.jsSDK。

3、在项目中引入SDK。

4、调用SDK中提供的API,例如获取歌曲详情。

5、在命令行工具中执行以下命令,启动Node.js服务器。

二、网易云音乐免费听歌模式在哪

网易云音乐的免费听歌模式可以在其移动应用的“我的”页面中找到。

详细来说,用户首先需要打开网易云音乐的移动应用。在应用的主界面下方,有一个标签栏,其中包括“我的”这一选项。点击“我的”进入个人页面后,在页面上方通常会有一个“免费听歌”或者类似的按钮或入口。点击这个按钮,用户就可以进入免费听歌模式。

在免费听歌模式中,用户可以畅享一定数量的歌曲,而无需支付任何费用。这通常是网易云音乐为了吸引新用户或者回馈老用户而提供的一种福利。具体的免费听歌时长和歌曲数量可能会根据用户的等级、活跃度或者网易云音乐的促销活动而有所变化。

开源的网易云音乐API项目都是怎么实现的?

2022-07-0519:58·街角小林上一篇文章这个高颜值的开源第三方网易云音乐播放器你值得拥有[1]介绍了一个开源的第三方网易云音乐播放器,这篇文章我们来详细了解一下其中使用到的网易云音乐api项目NeteaseCloudMusicApi[2]的实现原理。

NeteaseCloudMusicApi[3]使用Node.js开发,主要用到的框架和库有两个,一个Web应用开发框架Express[4],一个请求库Axios[5],这两个大家应该都很熟了就不过多介绍了。

创建express应用项目的入口文件为/app.js:

asyncfunctionstart(){require('./server').serveNcmApi({checkVersion:true,})}start()调用了/server.js文件的serveNcmApi方法,让我们转到这个文件,serveNcmApi方法简化后如下:

asyncfunctionserveNcmApi(options){constport=Number(options.port||process.env.PORT||'3000')consthost=options.host||process.env.HOST||''constapp=awaitconsturctServer(options.moduleDefs)constappExt=appappExt.server=app.listen(port,host,()=>{console.log(`serverrunning@http://${host?host:'localhost'}:${port}`)})returnappExt}主要是启动监听指定端口,所以创建应用的主要逻辑在consturctServer方法:

asyncfunctionconsturctServer(moduleDefs){//创建一个应用constapp=express()//设置为true,则客户端的IP地址被理解为X-Forwarded-*报头中最左边的条目app.set('trustproxy',true)/***配置CORS预检请求*/app.use((req,res,next)=>{if(req.path!=='/'!req.path.includes('.')){res.set({'Access-Control-Allow-Credentials':true,//跨域情况下,允许客户端携带验证信息,比如cookie,同时,前端发送请求时也需要设置withCredentials:true'Access-Control-Allow-Origin':req.headers.origin||'*',//允许跨域请求的域名,设置为*代表允许所有域名'Access-Control-Allow-Headers':'X-Requested-With,Content-Type',//用于给预检请求(options)列出服务端允许的自定义标头,如果前端发送的请求中包含自定义的请求标头,且该标头不包含在Access-Control-Allow-Headers中,那么该请求无法成功发起'Access-Control-Allow-Methods':'PUT,POST,GET,DELETE,OPTIONS',//设置跨域请求允许的请求方法理想'Content-Type':'application/json;charset=utf-8',//设置响应数据的类型及编码})}//OPTIONS为预检请求,复杂请求会在发送真正的请求前先发送一个预检请求,获取服务器支持的Access-Control-Allow-xxx相关信息,判断后续是否有必要再发送真正的请求,返回状态码204代表请求成功,但是没有内容req.method==='OPTIONS'?res.status(204).end():next()})//...}首先创建了一个Express应用,然后设置为信任代理,在Express里获取ip一般是通过req.ip和req.ips,trustproxy默认值为false,这种情况下req.ips值是空的,当设置为true时,req.ip的值会从请求头X-Forwarded-For上取最左侧的一个值,req.ips则会包含X-Forwarded-For头部的所有ip地址。

X-Forwarded-For头部的格式如下:

X-Forwarded-For:client1,proxy1,proxy2值通过一个逗号+空格把多个ip地址区分开,最左边的client1是最原始客户端的ip地址,代理服务器每成功收到一个请求,就把请求来源ip地址添加到右边。

以上面为例,这个请求通过了两台代理服务器:proxy1和proxy2。请求由client1发出,此时XFF是空的,到了proxy1时,proxy1把client1添加到XFF中,之后请求发往proxy2,通过proxy2的时候,proxy1被添加到XFF中,之后请求发往最终服务器,到达后proxy2被添加到XFF中。

但是伪造这个字段非常容易,所以当代理不可信时,这个字段也不一定可靠,不过正常情况下XFF中最后一个ip地址肯定是最后一个代理服务器的ip地址,这个会比较可靠。

随后设置了跨域响应头,这里的设置就是允许不同域名的网站也能请求成功的关键所在。

继续:

asyncfunctionconsturctServer(moduleDefs){//.../***解析Cookie*/app.use((req,_,next)=>{req.cookies={}//;(req.headers.cookie||'').split(/\\s*;\\s*/).forEach((pair)=>{//Polynomialregularexpression////从请求头中读取cookie,cookie格式为:name=value;name2=value2...,所以先根据;切割为数组;(req.headers.cookie||'').split(/;\\s+|(?<!\\s)\\s+$/g).forEach((pair)=>{letcrack=pair.indexOf('=')//没有值的直接跳过if(crack<1||crack==pair.length-1)return//将cookie保存到cookies对象上req.cookies[decode(pair.slice(0,crack)).trim()]=decode(pair.slice(crack+1),).trim()})next()})/***请求体解析和文件上传处理*/app.use(express.json())app.use(express.urlencoded({extended:false}))app.use(fileUpload())/***将public目录下的文件作为静态文件提供*/app.use(express.static(path.join(__dirname,'public')))/***缓存请求,两分钟内同样的请求会从缓存里读取数据,不会向网易云音乐服务器发送请求*/app.use(cache('2minutes',(_,res)=>res.statusCode===200))//...}接下来注册了一些中间件,用来解析cookie、处理请求体等,另外还做了接口缓存,防止太频繁请求网易云音乐服务器导致被封掉。

继续:

asyncfunctionconsturctServer(moduleDefs){//.../***特殊路由*/constspecial={'daily_signin.js':'/daily_signin','fm_trash.js':'/fm_trash','personal_fm.js':'/personal_fm',}/***加载/module目录下的所有模块,每个模块对应一个接口*/constmoduleDefinitions=moduleDefs||(awaitgetModulesDefinitions(path.join(__dirname,'module'),special))//...}接下来加载了/module目录下所有的模块:

每个模块代表一个对网易云音乐接口的请求,比如获取专辑详情的album_detail.js:

模块加载方法getModulesDefinitions如下:

asyncfunctiongetModulesDefinitions(modulesPath,specificRoute,doRequire=true,){constfiles=awaitfs.promises.readdir(modulesPath)constparseRoute=(fileName)=>specificRoutefileNameinspecificRoute?specificRoute[fileName]:`/${fileName.replace(/\\.js$/i,'').replace(/_/g,'/')}`//遍历目录下的所有文件constmodules=files.reverse().filter((file)=>file.endsWith('.js'))//过滤出js文件.map((file)=>{constidentifier=file.split('.').shift()//模块标识constroute=parseRoute(file)//模块对应的路由constmodulePath=path.join(modulesPath,file)//模块路径constmodule=doRequire?require(modulePath):modulePath//加载模块return{identifier,route,module}})returnmodules}以刚才的album_detail.js模块为例,返回的数据如下:

{identifier:'album_detail',route:'/album/detail',module:()=>{/*模块内容*/}}接下来就是注册路由:

asyncfunctionconsturctServer(moduleDefs){//...for(constmoduleDefofmoduleDefinitions){//注册路由app.use(moduleDef.route,async(req,res)=>{//cookie也可以从查询参数、请求体上传来;[req.query,req.body].forEach((item)=>{if(typeofitem.cookie==='string'){//将cookie字符串转换成json类型item.cookie=cookieToJson(decode(item.cookie))}})//把cookie、查询参数、请求头、文件都整合到一起,作为参数传给每个模块letquery=Object.assign({},{cookie:req.cookies},req.query,req.body,req.files,)try{//执行模块方法,即发起对网易云音乐接口的请求constmoduleResponse=awaitmoduleDef.module(query,(...params)=>{//参数注入客户端IPconstobj=[...params]//处理ip,为了实现IPv4-IPv6互通,IPv4地址前会增加::ffff:letip=req.ipif(ip.substr(0,7)=='::ffff:'){ip=ip.substr(7)}obj[3]={...obj[3],ip,}returnrequest(...obj)})//请求成功后,获取响应中的cookie,并且通过Set-Cookie响应头来将这个cookie设置到前端浏览器上constcookies=moduleResponse.cookieif(Array.isArray(cookies)cookies.length>0){if(req.protocol==='https'){//去掉跨域请求cookie的SameSite限制,这个属性用来限制第三方Cookie,从而减少安全风险res.append('Set-Cookie',cookies.map((cookie)=>{returncookie+';SameSite=None;Secure'}),)}else{res.append('Set-Cookie',cookies)}}//回复请求res.status(moduleResponse.status).send(moduleResponse.body)}catch(moduleResponse){//请求失败处理//没有响应体,返回404if(!moduleResponse.body){res.status(404).send({code:404,data:null,msg:'NotFound',})return}//301代表调用了需要登录的接口,但是并没有登录if(moduleResponse.body.code=='301')moduleResponse.body.msg='需要登录'res.append('Set-Cookie',moduleResponse.cookie)res.status(moduleResponse.status).send(moduleResponse.body)}})}returnapp}逻辑很清晰,将每个模块都注册成一个路由,接收到对应的请求后,将cookie、查询参数、请求体等都传给对应的模块,然后请求网易云音乐的接口,如果请求成功了,那么处理一下网易云音乐接口返回的cookie,最后将数据都返回给前端即可,如果接口失败了,那么也进行对应的处理。

其中从请求的查询参数和请求体里获取cookie可能不是很好理解,因为cookie一般是从请求体里带过来,这么做应该主要是为了支持在Node.js里调用:

请求成功后,返回的数据里如果存在cookie,那么会进行一些处理,首先如果是https的请求,那么会设置SameSite=None;Secure,SameSite是Cookie中的一个属性,用来限制第三方Cookie,从而减少安全风险。Chrome51开始新增这个属性,用来防止CSRF攻击和用户追踪,有三个可选值:strict/lax/none,默认为lax,比如在域名为https://123.com的页面里调用https://456.com域名的接口,默认情况下除了导航到123网址的get请求除外,其他请求都不会携带123域名的cookie,如果设置为strict更严格,完全不会携带cookie,所以这个项目为了方便跨域调用,设置为none,不进行限制,设置为none的同时需要设置Secure属性。

最后通过Set-Cookie响应头将cookie写入前端的浏览器即可。

发送请求接下来看一下上面涉及到发送请求所使用的request方法,这个方法在/util/request.js文件,首先引入了一些模块:

constencrypt=require('./crypto')constaxios=require('axios')constPacProxyAgent=require('pac-proxy-agent')consthttp=require('http')consthttps=require('https')consttunnel=require('tunnel')const{URLSearchParams,URL}=require('url')constconfig=require('../util/config.json')//...然后就是具体发送请求的方法createRequest,这个方法也挺长的,我们慢慢来看:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{letheaders={'User-Agent':chooseUserAgent(options.ua)}//...})}函数会返回一个Promise,首先定义了一个请求头对象,并添加了User-Agent头,这个头部会保存浏览器类型、版本号、渲染引擎,以及操作系统、版本、CPU类型等信息,标准格式为:

浏览器标识(操作系统标识;加密等级标识;浏览器语言)渲染引擎标识版本信息不用多说,伪造这个头显然是用来欺骗服务器,让它认为这个请求是来自浏览器,而不是同样也来自服务端。

默认写死了几个User-Agent头部随机进行选择:

constchooseUserAgent=(ua=false)=>{constuserAgentList={mobile:['Mozilla/5.0(iPhone;CPUiPhoneOS13_5_1likeMacOSX)AppleWebKit/605.1.15(KHTML,likeGecko)Version/13.1.1Mobile/15E148Safari/604.1','Mozilla/5.0(Linux;Android9;PCT-AL10)AppleWebKit/537.36(KHTML,likeGecko)Chrome/70.0.3538.64HuaweiBrowser/10.0.3.311MobileSafari/537.36',//...],pc:['Mozilla/5.0(Macintosh;IntelMacOSX10.15;rv:80.0)Gecko/20100101Firefox/80.0','Mozilla/5.0(WindowsNT10.0;Win64;x64;rv:80.0)Gecko/20100101Firefox/80.0',//...],}letrealUserAgentList=userAgentList[ua]||userAgentList.mobile.concat(userAgentList.pc)return['mobile','pc',false].indexOf(ua)>-1?realUserAgentList[Math.floor(Math.random()*realUserAgentList.length)]:ua}继续看:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{//...//如果是post请求,修改编码格式if(method.toUpperCase()==='POST')headers['Content-Type']='application/x-www-form-urlencoded'//伪造Referer头if(url.includes('music.163.com'))headers['Referer']='https://music.163.com'//设置ip头部letip=options.realIP||options.ip||''if(ip){headers['X-Real-IP']=ipheaders['X-Forwarded-For']=ip}//...})}继续设置了几个头部字段,Axios默认的编码格式为json,而POST请求一般都会使用application/x-www-form-urlencoded编码格式。

Referer头代表发送请求时所在页面的url,比如在https://123.com页面内调用https://456.com接口,Referer头会设置为https://123.com,这个头部一般用来防盗链。所以伪造这个头部也是为了欺骗服务器这个请求是来自它们自己的页面。

接下来设置了两个ip头部,realIP需要前端手动传递:

继续:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{//...//设置cookieif(typeofoptions.cookie==='object'){if(!options.cookie.MUSIC_U){//游客if(!options.cookie.MUSIC_A){options.cookie.MUSIC_A=config.anonymous_token}}headers['Cookie']=Object.keys(options.cookie).map((key)=>encodeURIComponent(key)+'='+encodeURIComponent(options.cookie[key]),).join(';')}elseif(options.cookie){headers['Cookie']=options.cookie}//...})}接下来设置cookie,分两种类型,一种是对象类型,这种情况cookie一般来源于查询参数或者请求体,另一种为字符串,这个就是正常情况下请求头带过来的。MUSIC_U应该就是登录后的cookie了,MUSIC_A应该是一个token,未登录情况下调用某些接口可能报错,所以会设置一个游客token:

继续:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{//...if(options.crypto==='weapi'){letcsrfToken=(headers['Cookie']||'').match(/_csrf=([^(;|$)]+)/)data.csrf_token=csrfToken?csrfToken[1]:''data=encrypt.weapi(data)url=url.replace(/\\w*api/,'weapi')}elseif(options.crypto==='linuxapi'){data=encrypt.linuxapi({method:method,url:url.replace(/\\w*api/,'api'),params:data,})headers['User-Agent']='Mozilla/5.0(X11;Linuxx86_64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/60.0.3112.90Safari/537.36'url='https://music.163.com/api/linux/forward'}elseif(options.crypto==='eapi'){constcookie=options.cookie||{}constcsrfToken=cookie['__csrf']||''constheader={osver:cookie.osver,//系统版本deviceId:cookie.deviceId,//encrypt.base64.encode(imei+'\\t02:00:00:00:00:00\\t5106025eb79a5247\\t70ffbaac7')appver:cookie.appver||'8.7.01',//app版本versioncode:cookie.versioncode||'140',//版本号mobilename:cookie.mobilename,//设备modelbuildver:cookie.buildver||Date.now().toString().substr(0,10),resolution:cookie.resolution||'1920x1080',//设备分辨率__csrf:csrfToken,os:cookie.os||'android',channel:cookie.channel,requestId:`${Date.now()}_${Math.floor(Math.random()*1000).toString().padStart(4,'0')}`,}if(cookie.MUSIC_U)header['MUSIC_U']=cookie.MUSIC_Uif(cookie.MUSIC_A)header['MUSIC_A']=cookie.MUSIC_Aheaders['Cookie']=Object.keys(header).map((key)=>encodeURIComponent(key)+'='+encodeURIComponent(header[key]),).join(';')data.header=headerdata=encrypt.eapi(options.url,data)url=url.replace(/\\w*api/,'eapi')}//...})}这一段代码会比较难理解,笔者也没有看懂,反正大致呢这个项目使用了四种类型网易云音乐的接口:weapi、linuxapi、eapi、api,比如:

https://music.163.com/weapi/vipmall/albumproduct/detailhttps://music.163.com/eapi/activate/initProfilehttps://music.163.com/api/album/detail/dynamic每种类型的接口请求参数、加密方式都不一样,所以需要分开单独处理:

比如weapi:

letcsrfToken=(headers['Cookie']||'').match(/_csrf=([^(;|$)]+)/)data.csrf_token=csrfToken?csrfToken[1]:''data=encrypt.weapi(data)url=url.replace(/\\w*api/,'weapi')将cookie中的_csrf值取出加到请求数据中,然后加密数据:

constweapi=(object)=>{consttext=JSON.stringify(object)constsecretKey=crypto.randomBytes(16).map((n)=>base62.charAt(n%62).charCodeAt())return{params:aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text),'cbc',presetKey,iv).toString('base64'),),'cbc',secretKey,iv,).toString('base64'),encSecKey:rsaEncrypt(secretKey.reverse(),publicKey).toString('hex'),}}查看其他加密算法:crypto.js[6]

至于这些是怎么知道的呢,要么就是网易云音乐内部人士(基本不可能),要么就是进行逆向了,比如网页版的接口,打开控制台,发送请求,找到在源码中的位置,打断点,查看请求数据结构,阅读压缩或混淆后的源码慢慢进行尝试,总之,向这些大佬致敬。

继续:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{//...//响应的数据结构constanswer={status:500,body:{},cookie:[]}//请求配置letsettings={method:method,url:url,headers:headers,data:newURLSearchParams(data).toString(),httpAgent:newhttp.Agent({keepAlive:true}),httpsAgent:newhttps.Agent({keepAlive:true}),}if(options.crypto==='eapi')settings.encoding=null//配置代理if(options.proxy){if(options.proxy.indexOf('pac')>-1){settings.httpAgent=newPacProxyAgent(options.proxy)settings.httpsAgent=newPacProxyAgent(options.proxy)}else{constpurl=newURL(options.proxy)if(purl.hostname){constagent=tunnel.httpsOverHttp({proxy:{host:purl.hostname,port:purl.port||80,},})settings.httpsAgent=agentsettings.httpAgent=agentsettings.proxy=false}else{console.error('代理配置无效,不使用代理')}}}else{settings.proxy=false}if(options.crypto==='eapi'){settings={...settings,responseType:'arraybuffer',}}//...})}这里主要是定义了响应的数据结构、定义了请求的配置数据,以及针对eapi做了一些特殊处理,最主要是代理的相关配置。

Agent是Node.js的HTTP模块中的一个类,负责管理http客户端连接的持久性和重用。它维护一个给定主机和端口的待处理请求队列,为每个请求重用单个套接字连接,直到队列为空,此时套接字要么被销毁,要么放入池中,在池里会被再次用于请求到相同的主机和端口,总之就是省去了每次发起http请求时需要重新创建套接字的时间,提高效率。

pac指代理自动配置,其实就是包含了一个javascript函数的文本文件,这个函数会决定是直接连接还是通过某个代理连接,比直接写死一个代理方便一点,当然需要配置的options.proxy是这个文件的远程地址,格式为:'pac+【pac文件地址】+'。pac-proxy-agent模块会提供一个http.Agent实现,它会根据指定的PAC代理文件判断使用哪个HTTP、HTTPS或SOCKS代理,或者是直接连接。

至于为什么要使用tunnel模块,笔者搜索了一番还是没有搞懂,可能是解决http协议的接口请求网易云音乐的https协议接口失败的问题?知道的朋友可以评论区解释一下~

最后:

constcreateRequest=(method,url,data={},options)=>{returnnewPromise((resolve,reject)=>{//...axios(settings).then((res)=>{constbody=res.data//将响应的set-cookie头中的cookie取出,直接保存到响应对象上answer.cookie=(res.headers['set-cookie']||[]).map((x)=>x.replace(/\\s*Domain=[^(;|$)]+;*/,''),//去掉域名限制)try{//eapi返回的数据也是加密的,需要解密if(options.crypto==='eapi'){answer.body=JSON.parse(encrypt.decrypt(body).toString())}else{answer.body=body}answer.status=answer.body.code||res.status//统一这些状态码为200,都代表成功if([201,302,400,502,800,801,802,803].indexOf(answer.body.code)>-1){//特殊状态码answer.status=200}}catch(e){try{answer.body=JSON.parse(body.toString())}catch(err){answer.body=body}answer.status=res.status}answer.status=100<answer.statusanswer.status<600?answer.status:400//状态码200代表成功,其他都代表失败if(answer.status===200)resolve(answer)elsereject(answer)}).catch((err)=>{answer.status=502answer.body={code:502,msg:err}reject(answer)})})}最后一步就是使用Axios发送请求了,处理了一下响应的cookie,保存到响应对象上,方便后续使用,另外处理了一些状态码,可以看到try-catch的使用比较多,至于为什么呢,估计要多尝试来能知道到底哪里会出错了,有兴趣的可以自行尝试。

总结本文通过源码角度了解了一下NeteaseCloudMusicApi[7]项目的实现原理,可以看到整个流程是比较简单的。无非就是一个请求代理,难的在于找出这些接口,并且逆向分析出每个接口的参数,加密方法,解密方法。最后也提醒一下,这个项目仅供学习使用,请勿从事商业行为或进行破坏版权行为~

参考资料[1]

这个高颜值的开源第三方网易云音乐播放器你值得拥有:https://juejin.cn/post/7116521655844732936

[2]

NeteaseCloudMusicApi:https://github.com/Binaryify/NeteaseCloudMusicApi

[3]

NeteaseCloudMusicApi:https://github.com/Binaryify/NeteaseCloudMusicApi

[4]

Express:https://www.expressjs.com.cn/

[5]

Axios:https://www.axios-http.cn/

[6]

crypto.js:https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js

[7]

NeteaseCloudMusicApi:https://github.com/Binaryify/NeteaseCloudMusicApi

【GSFAI BANK FINANCING】尊享直接对接老板

电话+V: 152079-09430

专注于自媒体运营推广配套流程服务方案。为企业及个人客户提供了高性价比的运营方案,解决小微企业和个人创业难的问题

网易云音乐的免费音乐 API
Copyright2023未知推广科技