为了请求QQ音乐的数据时的跨域问题,我们使用Node.js + Express搭建一个中间件
可以直接在github: https://github.com/liaoqinwei/qqMusicApi 拔取源码
写的过程中借助文章 整理的接口(需要原生的qq音乐接口即可访问)https://blog.csdn.net/weixin_33874713/article/details/88003925
一、安装配置
npm install express 配置服务
npm install body-parser 解析参数
npm install fs 用于读取文件
npm install js-base64 base64解密
npm install mime mime参数类型解析
二、文件夹搭建
三、搭建工具文件Utils
promiseHttps.js
用于帮我们基于Promise发送Http/https请求、解析参数、配置请求头。
const https = require('https'),
http = require('http');
/* 处理参数
* url: 拼接的路径
* param: 参数[Object]
* */
let urlHandler = (url, param = {}) => {
let result = ''
for (let key in param) {
// 如果是自身属性, 避免遍历到原型
if (param.hasOwnProperty(key)) {
result += `&${key}=${param[key]}`
}
}
return url.indexOf('?') > -1 ? `${url}&${result}` : `${url}?${result.slice(1)}`
}
/*
* 用于发送请求 返回一个promise实例
*
* */
let getData = (config = {}) => {
// 没有传参就抛出异常
if (Object.keys(config).length === 0) {
throw new Error('Please pass in parameters !')
}
/* 解构参数:url请求地址 params请求参数 headers请求头信息 hostname请求的主机名 */
let {url, params = {}, headers = {Connection: 'keep-alive', Accept: '*/*'}, hostname = 'c.y.qq.com'} = config,
path = urlHandler(url, params), // 解析参数
option = {
hostname,
path,
headers
}
return new Promise((resolve) => {
// 发送https请求
https.get(option, res => {
// 接收数据
let chunk = ''
// 数据是流传输 所以我们要监听 data 事件
res.on('data', result => {
// console.log(result)
chunk += result + ''
})
// 数据传输完成触发end 数据完了我们执行 resolve 方法
res.on('end', () => {
resolve(chunk)
})
})
})
}
/*
* 获取文件
* */
let getFile = url => {
if (!url) return;
return new Promise(resolve => {
http.get(url, res => {
let list = [], file
// 我们用数组把所有的流 存储起来
res.on('data', result => {
list.push(result)
})
// 将所有的流 拼接成一个流 然后返回调用 resolve
res.on('end', () => {
file = Buffer.concat(list)
resolve(file)
})
})
})
}
module.exports = {getData, getFile}
promiseFS.js
帮助我们基于Promise读取文件
/*
* 将fs中的常用 I/O 操作封装为promise版本
* */
let fs = require('fs'),
path = require('path'),
resultObj = {};
let suffixHandle = (pathname) => {
let suffixReg = /\.(PNG|JPG|JPEG|WEBP|ICO|BMP|SVG|MP4|MP3|M3U8|WAV|OGG)$/i
return suffixReg.test(pathname)
}
// READ-FILE / READ-DIR / MK-DIR / RM-DIR / UN-LINK /
/*
* 读取文件时 需要使用编码 以及过滤 富媒体 文件
* */
['readFile', 'readdir', 'mkdir', 'rmdir', 'unlink'].forEach(item => {
resultObj[item] = function (pathname, encoding = 'utf8') {
return new Promise((resolve, reject) => {
let callback = function (err, res) {
!err ? resolve(res) : reject(err)
}
// 如果是富媒体编码就为null
suffixHandle() ? encoding = null : null
pathname = path.resolve(pathname)
if (item !== 'readFile') {
encoding = callback
encoding = null
}
fs[item](pathname, encoding, callback)
})
}
});
// WRITE-FILE / APPEND-FILE
['writeFile', 'appendFile'].forEach(item => {
resultObj[item] = function (pathname, content, encoding = 'utf8') {
// 支持JSON类型数据
(typeof content === 'object' && content !== null) ? content = JSON.stringify(content) : null
// 将传入的内容转为 对象
typeof content !== 'string' ? content += '' : null
return new Promise((resolve, reject) => {
let callback = function (err, res) {
!err ? resolve(res) : reject(err)
}
pathname = path.resolve(pathname)
fs[item](pathname, content, encoding, callback)
})
}
})
// COPYFILE
resultObj['copyFile'] = function (pathname1, pathname2) {
return new Promise((resolve, reject) => {
// 解析路径 为了防止路径错误
pathname1 = path.resolve(pathname1)
pathname2 = path.resolve(pathname2)
let callback = function (err) {
!err ? resolve() : reject(err)
}
fs['copyFile'](pathname1, pathname2, callback)
})
}
module.exports = resultObj
parseSign.js
帮助我们请求歌词时加密必要的请求参数
由于该文件是从qq音乐请求中拿下来的,文件经过打包,并非作者所写,所以直接放上文件地址:https://github.com/liaoqinwei/qqMusicApi/blob/master/utils/parseSign.js
三、发送请求
request/index.js 请求移动端的数据
说明:当我们请求qq音乐的时候,移动端的数据会放在window.__INIT_DATA__中所以我们需要通过正则截取中间的数据为我们所用(我们将数据放在了json文件中)
let {getData} = require('../utils/promiseHttps'),
{writeFile} = require('../utils/promiseFS');
// 移动端主页数据思路
// 我们把数据获取下来 通过 正则拆分数据
// 将数据保存到json/recommend.json 中
// 为了保证数据真实,我们需要每隔一分钟执行一次这个方法
let saveRecommendData = () => {
getData({url: 'https://i.y.qq.com/n2/m/index.html'}).then(res => {
let reg = /<script>window\.__INIT_DATA__=(.*?)<\/script>/,
data = reg.exec(res)[1];
// 存数据
writeFile('./json/recommend.json', data).then(() => {
console.log('推荐数据更新成功')
})
})
}
// 排行榜数据
let saveTopListData = () => {
getData({url: 'https://i.y.qq.com/n2/m/index.html?tab=toplist'}).then(res => {
let reg = /<script>window\.__INIT_DATA__=(.*?)<\/script>/,
data = reg.exec(res)[1];
// 存数据
writeFile('./json/tolist.json', data).then(() => {
console.log('排行数据更新成功')
})
})
}
module.exports = {
saveRecommendData,
saveTopListData
}
request/song.js 请求关于歌曲的数据
parse: 用于加密sign参数的
Base64: 我们获取的歌词是通过base64加密的
/*
* 封装歌曲数据
* */
let promiseHttps = require('../utils/promiseHttps'),
{parse} = require('../utils/parseSign'),
{Base64} = require('js-base64')
/*
* 获取歌曲数据
* songId: 歌曲的Id
* copyright: 不传 返回歌曲的详情信息 "albummid"
* 传 返回歌曲的 m4a文件所在的 json文件 "songmid"
* */
let getSongData = (songId, copyright) => {
let hostname = 'u.y.qq.com',
url = '/cgi-bin/musics.fcg',
params = {
g_tk: 1807347960,
// sign:'zzany09qmk055rgo2ide8eb7dce5d72490520aebab2de1fce2', // parse(data)
loginUin: 0,
hostUin: 0,
format: 'json',
inCharset: 'utf8',
outCharset: 'utf-8',
notice: 0,
platform: 'yqq.json',
needNewCode: 0,
data: `{"comm":{"ct":24,"cv":10000},"albumDetail":{"module":"music.musichallAlbum.AlbumInfoServer","method":"GetAlbumDetail","param":{"albumMid":"${songId}"}}}`,
_: Date.now()
};
// 判断是否有传入 copyright参数 如果有的话就是获取歌曲的m4a文件
if (copyright) {
params.data = `{"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"8058980168","songmid":["${songId}"],"songtype":[0],"uin":"0","loginflag":1,"platform":"20"}},"comm":{"uin":0,"format":"json","ct":24,"cv":0}}`
}
// 处理密钥
params.sign = parse(params.data);
return promiseHttps.getData({hostname, url, params})
}
/*
* 获取歌曲
* */
let getSongM4a = url => {
return promiseHttps.getFile(url)
}
/*
* 获取歌词
* */
let getLyric = songId => {
let url = '/lyric/fcgi-bin/fcg_query_lyric_new.fcg',
headers = {
referer: 'https://y.qq.com/portal/player.html'
},
params = {
g_tk: 1414077212,
g_tk_new_20200303: 1414077212,
format: 'json',
outCharset: 'utf-8',
notice: 0,
songmid: songId,
hostUin: 0,
inCharset: 'inCharset',
platform: 'yqq.json',
needNewCode: 0,
pcachetime: 1592041856541,
loginUin: 0
};
return promiseHttps.getData({url, params, headers}).then(result => {
result = JSON.parse(result);
result.lyric = Base64.decode(result.lyric)
return Promise.resolve(result)
})
}
module.exports = {
getSongData,
getSongM4a,
getLyric
}
专辑/搜索数据都是一样搭建的具体代码看github: https://github.com/liaoqinwei/qqMusicApi
搭建服务
server.js
用于开启服务
let express = require('express'),
index = require('./request'),
mime = require('mime'),
bodyParser = require('body-parser'),
routers = require('./router')
// 初始化
let app;
let init = () => {
// 启动服务器
app = express()
// 收到请求 调用写好的接口 响应数据
app.listen(9090, () => {
console.log('服务启动!')
})
// 配置
app.use(bodyParser.urlencoded({extended: false}))
app.all('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.status(200)
res.type(mime.getType('json'))
next()
})
app.use(routers)
// 设置一个定时器,每隔一个小时执行saveIndexData,保证主页获取到的数据的真实性
index.saveRecommendData()
index.saveTopListData()
setInterval(() => {
index.saveRecommendData()
index.saveTopListData()
}, 1000 * 60 * 60);
}
init()
router
接收请求返回数据 接口的二次封装
router/phoneIndexRouter.js
返回移动端主页数据
由于我们前面移动端数据特殊 所以我们将数据 从json文件中读取
let express = require('express'),
router = express.Router(),
promiseFs = require('../utils/promiseFS')
// 移动端
// 推荐数据( 移动端 )
router.get('/recommend', (req, res) => {
let {url} = req;
promiseFs.readFile('./json/recommend.json').then(result => {
res.send(result)
})
})
// 排行榜数据( 移动端 )
router.get('/tolist', (req, res) => {
let {url} = req;
promiseFs.readFile('./json/tolist.json').then(result => {
res.send(result)
})
})
module.exports = router
router/songRouter.js
返回歌曲相关的数据
let express = require('express'),
router = express.Router(),
song = require('../request/song'),
mime = require('mime')
// 返回歌曲m4a文件
router.get('/song', (req, res) => {
let songId = req.query.id
res.status(200)
res.type(mime.getType('m4a'))
if (!songId) {
res.send('请求错误,请传入参数')
} else {
song.getSongData(songId, 1).then(result => {
result = JSON.parse(result)
let data = result['req_0'].data,
url = data.sip[0] + data.midurlinfo[0].purl;
return song.getSongM4a(url)
}).then(result => {
res.send(result)
})
}
})
// 获取歌词
router.get('/lyric', (req, res) => {
let songId = req.query.id
if (!songId) {
res.send('请求错误,请传入参数')
} else {
song.getLyric(songId).then(result => {
res.send(result)
})
}
})
// 返回歌曲详情信息
router.get('/songDetail', (req, res) => {
let songId = req.query.id;
if (!songId) {
res.send('请求错误,请传入参数')
} else {
song.getSongData(songId).then(result => {
res.send(result)
})
}
})
module.exports = router
其他路由也大同小异, 代码量太多,请大家到github上获取
https://github.com/liaoqinwei/qqMusicApi/blob/master/utils/parseSign.js
转载:https://blog.csdn.net/weixin_45412353/article/details/106746509