NeteaseCloudMusicApi 开源 网易云nodeapi 源码分析

这篇文章,分享下网易云开源的一个api,通过伪造请求来获取网易云的歌曲,评论和电台等信息。
首先,大致描述项目里用到的一些知识点

涉及到的知识点
  • apicache 缓存中间件,可以用于redis,项目中的作用在于,避免频繁的请求网易云后台
  • 文件操作 项目中通过获取router文件加下的文件名来动态加载路由,一个文件名就是一个接口
  • crypto 加密模块,因为网易云网页api基本都是加密处理了 参考网易云音乐新版webApi分析
apicache 缓存中间件

api缓存中间件这个没什么可说的,为什么用到 缓存,觉得文档解释法就很好

Because route-caching of simple data/responses should ALSO be simple.

因为简单数据的和路由本来就是缓存提供。
apicache 是可配置的 可以单独为某一个接口请求缓存,也可以配置全局接口缓存,还可以对特定的响应缓存,如这个项目就是特定的响应缓存

const onlyStatus200 = (req, res) => res.statusCode === 200;

app.use(cache("2 minutes", onlyStatus200));

可以通过函数的形式来配置,可以说很强大了,针对接口响应的200俩缓存,还可以设置缓存时间,可以说很强大,要做详细的了解可以参考npm文档

文件操作来配置路由(接口)

这个项目中的router文件加下的一个文件就是一个接口,很清晰。具体看代码操作。

// 简化 路由 导出方式, 由这里统一对 router 目录中导出的路由做包装, 路由实际对应的文件只专注做它该做的事情, 不用重复写样板代码
const { createWebAPIRequest, request } = require("./util/util");
const Wrap = fn => (req, res) => fn(req, res, createWebAPIRequest, request);

// 同步读取 router 目录中的js文件, 根据命名规则, 自动注册路由
fs.readdirSync("./router/").reverse().forEach(file => {
    if (/\.js$/i.test(file) === false) {
        return;
    }

    let route;

    if (typeof UnusualRouteFileMap[file] !== "undefined") {
        route = UnusualRouteFileMap[file];
    } else {
        route =
            "/" +
            file
            .replace(/\.js$/i, "")
            .replace(/_/g, "/")
            .replace(/[A-Z]/g, a => {
                return "/" + a.toLowerCase();
            });
    }

    app.use(route, Wrap(require("./router/" + file)));
});

这里稍微解释下
app.use(route, Wrap(require("./router/" + file)));
这行代码,这里的形式看上去很复杂,Wrap是个什么鬼,你跑去一看Wrap这个函数定义
const Wrap = fn => (req, res) => fn(req, res, createWebAPIRequest, request);
这个看起来很头晕啊,那我们就从最普通的路由说起

app.use("路径",function(req,res){
    //处理请求代码
})

这样一看是不是有点对上头了,Wrap就是一个function(req,res){}的形式,只不过函数体是由require导出来的,这里点到为止,你在回头看wrap是不是有点柑橘了呢。

接口设计分析一波

以artist_list.js为例

//分类歌单
// 歌手分类
module.exports = (req, res, createWebAPIRequest, request) => {
    const cookie = req.get("Cookie") ? req.get("Cookie") : "";
    const data = {
        categoryCode: req.query.cat || "1001",
        offset: req.query.offset || 0,
        total: req.query.total ? "true" : "false",
        limit: req.query.limit || 30,
        initial: (req.query.initial || "").toUpperCase().charCodeAt() || ""
    };
    createWebAPIRequest(
        "music.163.com",
        "/weapi/artist/list",
        "POST",
        data,
        cookie,
        music_req => {
            res.send(music_req);
        },
        err => res.status(502).send("fetch error")
    );
};
  1. 获取cookie
  2. 请求和默认的参数合并
  3. 携带请求地址,方式(post),真实接口,调用工具的createWebAPIRequest()
通用的工具方法
const Encrypt = require("./crypto.js");
const request = require("request");
const querystring = require("querystring");

request.debug = true;

function randomUserAgent() {
  const userAgentList = [
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1",
    "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36",
    "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36",
    "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89;GameHelper",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:46.0) Gecko/20100101 Firefox/46.0",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:46.0) Gecko/20100101 Firefox/46.0",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
    "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
    "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)",
    "Mozilla/5.0 (Windows NT 6.3; Win64, x64; Trident/7.0; rv:11.0) like Gecko",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586",
    "Mozilla/5.0 (iPad; CPU OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1"
  ];
  const num = Math.floor(Math.random() * userAgentList.length);
  return userAgentList[num];
}

function createWebAPIRequest(
  host,
  path,
  method,
  data,
  cookie,
  callback,
  errorcallback
) {
  // console.log(cookie);
  if (cookie.match(/_csrf=[^(;|$)]+/g))
    data.csrf_token = cookie.match(/_csrf=[^(;|$)]+/g)[0].slice(6);
  else data.csrf_token = "";
  const proxy = cookie.split("__proxy__")[1];
  cookie = cookie.split("__proxy__")[0];
  const cryptoreq = Encrypt(data);
  const options = {
    url: `http://${host}${path}`,
    method: method,
    headers: {
      Accept: "*/*",
      "Accept-Language": "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4",
      Connection: "keep-alive",
      "Content-Type": "application/x-www-form-urlencoded",
      Referer: "http://music.163.com",
      Host: "music.163.com",
      Cookie: cookie,
      "User-Agent": randomUserAgent()
    },
    body: querystring.stringify({
      params: cryptoreq.params,
      encSecKey: cryptoreq.encSecKey
    }),
    proxy: proxy
  };
  console.log(
    `[request] ${options.method} ${options.url} proxy:${options.proxy}`
  );

  request(options, function(error, res, body) {
    if (error) {
      console.error(error);
      errorcallback(error);
    } else {
      //解决 网易云 cookie 添加 .music.163.com 域设置。
      //如: Domain=.music.163.com
      let cookie = res.headers["set-cookie"];
      if (Array.isArray(cookie)) {
        cookie = cookie
          .map(x => x.replace(/.music.163.com/g, ""))
          .sort((a, b) => a.length - b.length);
      }
      callback(body, cookie);
    }
  });
}

function createRequest(path, method, data) {
  return new Promise((resolve, reject) => {
    const options = {
      url: `http://music.163.com${path}`,
      method: method,
      headers: {
        Referer: "http://music.163.com",
        Cookie: "appver=1.5.2",
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": randomUserAgent()
      }
    };

    if (method.toLowerCase() === "post") {
      options.body = data;
    }

    request(options, function(error, res, body) {
      if (error) {
        reject(error);
      } else {
        resolve(body);
      }
    });
  });
}
module.exports = {
  request,
  createWebAPIRequest,
  createRequest
};

这个工具js直接看看核心方法


function createWebAPIRequest(
  host,
  path,
  method,
  data,
  cookie,
  callback,
  errorcallback
) {
  // console.log(cookie);
  if (cookie.match(/_csrf=[^(;|$)]+/g))
    data.csrf_token = cookie.match(/_csrf=[^(;|$)]+/g)[0].slice(6);
  else data.csrf_token = "";
  const proxy = cookie.split("__proxy__")[1];
  cookie = cookie.split("__proxy__")[0];
  const cryptoreq = Encrypt(data);
  const options = {
    url: `http://${host}${path}`,
    method: method,
    headers: {
      Accept: "*/*",
      "Accept-Language": "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4",
      Connection: "keep-alive",
      "Content-Type": "application/x-www-form-urlencoded",
      Referer: "http://music.163.com",
      Host: "music.163.com",
      Cookie: cookie,
      "User-Agent": randomUserAgent()
    },
    body: querystring.stringify({
      params: cryptoreq.params,
      encSecKey: cryptoreq.encSecKey
    }),
    proxy: proxy
  };
  console.log(
    `[request] ${options.method} ${options.url} proxy:${options.proxy}`
  );

  request(options, function(error, res, body) {
    if (error) {
      console.error(error);
      errorcallback(error);
    } else {
      //解决 网易云 cookie 添加 .music.163.com 域设置。
      //如: Domain=.music.163.com
      let cookie = res.headers["set-cookie"];
      if (Array.isArray(cookie)) {
        cookie = cookie
          .map(x => x.replace(/.music.163.com/g, ""))
          .sort((a, b) => a.length - b.length);
      }
      callback(body, cookie);
    }
  });
}

大概就是

  1. 仿照请求头参数
  2. 加密请求参数
  3. 发送http请求
发布了38 篇原创文章 · 获赞 14 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/sinat_23156865/article/details/81697413