为了请求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
在这里插入图片描述

Logo

前往低代码交流专区

更多推荐