Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

获取歌词改用酷我电脑客户端 #14

Open
UPman24 opened this issue Jan 29, 2023 · 1 comment
Open

获取歌词改用酷我电脑客户端 #14

UPman24 opened this issue Jan 29, 2023 · 1 comment

Comments

@UPman24
Copy link

UPman24 commented Jan 29, 2023

web官网歌词页经常奔溃,不稳定,下面是改用PC客户端稳定获取歌词的方法

1.在根目录引入如下 4 个包(由于酷我PC客户端歌词是加密的所以需要引入新的包进行解密)

npm install iconv-lite

npm install needle

npm install process

npm install zlib

2.找到 app => service => lrc.ts

3.替换代码(代码是兼容的不影响其他模块)

4.原本代码如下:

const BaseService = require('./BaseService')

export default class Lrc extends BaseService {
  async LrcRes (musicId) {
    return this.commonRequest(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${musicId}&httpsStatus=1`)
  }
}

5.将上面的代码替换成下面这样的代码

const BaseService = require('./BaseService')

const needle = require('needle')
const process = require('process')
const deflateRaw = require('zlib')
const { inflate } = require('zlib')
const iconv = require('iconv-lite')

const bufkey = Buffer.from('yeelion')
const bufkeylen = bufkey.length
const buildParams = (id, isGetLyricx) => {
  let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
  if (isGetLyricx) params += '&lrcx=1'
  const bufstr = Buffer.from(params)
  const bufstrlen = bufstr.length
  const output = new Uint16Array(bufstrlen)
  let i = 0
  while (i < bufstrlen) {
    let j = 0
    while (j < bufkeylen && i < bufstrlen) {
      output[i] = bufkey[j] ^ bufstr[i]
      i++
      j++
    }
  }
  return Buffer.from(output).toString('base64')
}

const cancelHttp = requestObj => {
  // console.log(requestObj)
  if (!requestObj) return
  // console.log('cancel:', requestObj)
  if (!requestObj.abort) return
  requestObj.abort()
}

const requestMsg = {
  fail: '请求异常,可以多试几次,若还是不行就换一首吧',
  unachievable: '哦No...接口无法访问了!',
  timeout: '请求超时',
  // unachievable: '哦No...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
  notConnectNetwork: '无法连接到服务器',
  cancelRequest: '取消http请求',
}

const request = (url, options, callback) => {
  let data
  if (options.body) {
    data = options.body
  } else if (options.form) {
    data = options.form
    // data.content_type = 'application/x-www-form-urlencoded'
    options.json = false
  } else if (options.formData) {
    data = options.formData
    // data.content_type = 'multipart/form-data'
    options.json = false
  }
  options.response_timeout = options.timeout

  return needle.request(options.method || 'get', url, data, options, (err, resp, body) => {
    if (!err) {
      body = resp.body = resp.raw.toString()
      try {
        resp.body = JSON.parse(resp.body)
      } catch (_) { }
      body = resp.body
    }
    callback(err, resp, body)
  }).request
}

const defaultHeaders = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
}

const handleDeflateRaw: any = data => new Promise((resolve, reject) => {
  deflateRaw(data, (err, buf) => {
    if (err) return reject(err)
    resolve(buf)
  })
})

const regx = /(?:\d\w)+/g

const fetchData = async (url, method, {
  headers = {},
  format = 'json',
  timeout = 15000,
  ...options
}, callback) => {
  headers = Object.assign({}, headers)
  const bHh = '624868746c'
  if (headers[bHh]) {
    const path = url.replace(/^https?:\/\/[\w.:]+\//, '/')
    let s = Buffer.from(bHh, 'hex').toString()
    s = s.replace(s.substr(-1), '')
    s = Buffer.from(s, 'base64').toString()
    const v = process.versions.app.split('-')[0].split('.').map(n => n.length < 3 ? n.padStart(3, '0') : n).join('')
    const v2 = process.versions.app.split('-')[1] || ''
    headers[s] = !s || `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`
    delete headers[bHh]
  }
  return request(url, {
    ...options,
    method,
    headers: Object.assign({}, defaultHeaders, headers),
    timeout,
    json: format === 'json',
  }, (err, resp, body) => {
    if (err) return callback(err, null)
    callback(null, resp, body)
  })
}

const buildHttpPromose = (url, options) => {
  const obj: any = {
    isCancelled: false,
  }
  obj.promise = new Promise((resolve, reject) => {
    obj.cancelFn = reject
    // console.log(`\nsend request---${url}`)
    fetchData(url, options.method, options, (err, resp) => {
      // options.isShowProgress && window.api.hideProgress()
      //   console.log(`\nresponse---${url}`)
      //   console.log(body)
      obj.requestObj = null
      obj.cancelFn = null
      if (err) return reject(err)
      resolve(resp)
    }).then(ro => {
      obj.requestObj = ro
      if (obj.isCancelled) obj.cancelHttp()
    })
  })
  obj.cancelHttp = () => {
    if (!obj.requestObj) return obj.isCancelled = true
    cancelHttp(obj.requestObj)
    obj.requestObj = null
    obj.promise = obj.cancelHttp = null
    obj.cancelFn(new Error(requestMsg.cancelRequest))
    obj.cancelFn = null
  }
  return obj
}
const httpFetch = (url, options = { method: 'get' }) => {
  const requestObj = buildHttpPromose(url, options)
  requestObj.promise = requestObj.promise.catch(err => {
    if (err.message === 'socket hang up') {
      return Promise.reject(new Error(requestMsg.unachievable))
    }
    switch (err.code) {
      case 'ETIMEDOUT':
      case 'ESOCKETTIMEDOUT':
        return Promise.reject(new Error(requestMsg.timeout))
      case 'ENOTFOUND':
        return Promise.reject(new Error(requestMsg.notConnectNetwork))
      default:
        return Promise.reject(err)
    }
  })
  return requestObj
}
const lrcTools: any = {
  rxps: {
    wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/,
    tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/,
    wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g,
    wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/,
  },
  offset: 1,
  offset2: 1,
  isOK: false,
  lines: [],
  tags: [],
  getWordInfo(str, str2, prevWord) {
    const offset = parseInt(str)
    const offset2 = parseInt(str2)
    const startTime = Math.abs((offset + offset2) / (this.offset * 2))
    const endTime = Math.abs((offset - offset2) / (this.offset2 * 2)) + startTime
    if (prevWord) {
      if (startTime < prevWord.endTime) {
        prevWord.endTime = startTime
        if (prevWord.startTime > prevWord.endTime) {
          prevWord.startTime = prevWord.endTime
        }
        prevWord.newTimeStr = ''
      }
    }
    return {
      startTime,
      endTime,
      timeStr: '',
    }
  },
  parseLine(line) {
    if (line.length < 6) return
    let result = this.rxps.wordLine.exec(line)
    if (result) {
      const time = result[1]
      let words = result[2]
      if (words == null) {
        words = ''
      }
      const wordTimes = words.match(this.rxps.wordTimeAll)
      if (!wordTimes) return
      // console.log(wordTimes)
      let preTimeInfo
      for (const timeStr of wordTimes) {
        const result = this.rxps.wordTime.exec(timeStr)
        const wordInfo = this.getWordInfo(result[1], result[2], preTimeInfo)
        words = words.replace(timeStr, wordInfo.timeStr)
        if (preTimeInfo?.newTimeStr) words = words.replace(preTimeInfo.timeStr, preTimeInfo.newTimeStr)
        preTimeInfo = wordInfo
      }
      this.lines.push(time + words)
      return
    }
    result = this.rxps.tagLine.exec(line)
    if (!result) return
    if (result[1] === 'kuwo') {
      let content = result[2]
      if (content !== null && content.includes('][')) {
        content = content.substring(0, content.indexOf(']['))
      }
      const valueOf = parseInt(content, 8)
      this.offset = Math.trunc(valueOf / 10)
      this.offset2 = Math.trunc(valueOf % 10)
      if (this.offset === 0 || Number.isNaN(this.offset) || this.offset2 === 0 || Number.isNaN(this.offset2)) {
        this.isOK = false
      }
    } else {
      this.tags.push(line)
    }
  },
  parse(lrc) {
    // console.log(lrc)
    const lines = lrc.split(/\r\n|\r|\n/)
    const tools = Object.create(this)
    tools.isOK = true
    tools.offset = 1
    tools.offset2 = 1
    tools.lines = []
    tools.tags = []
    for (const line of lines) {
      if (!tools.isOK) throw new Error('failed')
      tools.parseLine(line)
    }
    if (!tools.lines.length) return ''
    let lrcs = tools.lines.join('\n')
    if (tools.tags.length) lrcs = `${tools.tags.join('\n')}\n${lrcs}`
    // console.log(lrcs)
    return lrcs
  },
}
const isGetLyricx = true
const handleInflate = data => new Promise((resolve, reject) => {
  inflate(data, (err, result) => {
    if (err) return reject(err)
    resolve(result)
  })
})
const bufKey = Buffer.from('yeelion')
const bufKeyLen = bufKey.length
const decodeLyrics = async (buf, isGetLyricx) => {
  if (buf.toString('utf8', 0, 10) !== 'tp=content') return ''
  const lrcData: any = await handleInflate(buf.slice(buf.indexOf('\r\n\r\n') + 4))
  if (!isGetLyricx) return iconv.decode(lrcData, 'gb18030')
  const bufStr = Buffer.from(lrcData.toString(), 'base64')
  const bufStrLen = bufStr.length
  const output = new Uint16Array(bufStrLen)
  let i = 0
  while (i < bufStrLen) {
    let j = 0
    while (j < bufKeyLen && i < bufStrLen) {
      output[i] = bufStr[i] ^ bufKey[j]
      i++
      j++
    }
  }
  return iconv.decode(Buffer.from(output), 'gb18030')
}
const timeExp = /^\[([\d:.]*)\]{1}/g
const sortLrcArr = (arr) => {
  const lrcSet = new Set()
  const lrc: any = []
  const lrcT: any = []
  for (const item of arr) {
    if (lrcSet.has(item.time)) {
      if (lrc.length < 2) continue
      const tItem: any = lrc.pop()
      tItem.time = lrc[lrc.length - 1].time
      lrcT.push(tItem)
      lrc.push(item)
    } else {
      lrc.push(item)
      lrcSet.add(item.time)
    }
  }
  return {
    lrc,
    lrcT,
  }
}
const parseLrc = (lrc) => {
  const lines = lrc.split(/\r\n|\r|\n/)
  const tags: any = []
  const lrcArr: any = []
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trim()
    const result = timeExp.exec(line)
    if (result) {
      let text = line.replace(timeExp, '').trim()
      let time = RegExp.$1
      if (/\.\d\d$/.test(time)) time += '0'
      const regexp = /<.*?>/g
      text = text.replace(regexp, '').replace(/\[by:.*?\](\n|$)/g, '').replace(/\[kuwo:.*?\](\n|$)/g, '')
      const times = time.split(':');
      time = (parseFloat(times[0]) * 60 + parseFloat(times[1])).toFixed(2);
      lrcArr.push({
        time,
        lineLyric: text,
      })
    } else if (lrcTools.rxps.tagLine.test(line)) {
      tags.push(line)
    }
  }
  const lrcInfo = sortLrcArr(lrcArr)
  return lrcInfo
}

const rendererInvoke = async (params) => {
  const lrc = await decodeLyrics(Buffer.from(params.lrcBase64, 'base64'), isGetLyricx)
  return Buffer.from(lrc).toString('base64')
}
const decodeLyric = base64Data => rendererInvoke(base64Data)

export default class Lrc extends BaseService {
  async LrcRes(musicId) {
    const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicId, isGetLyricx)}`)
    requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {
      if (statusCode !== 200) return Promise.reject(new Error(JSON.stringify(body)))
      return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
        let lrcInfo
        lrcInfo = parseLrc(Buffer.from(base64Data, 'base64').toString())
        try {
          lrcInfo = parseLrc(Buffer.from(base64Data, 'base64').toString())
        } catch (err) {
          return Promise.reject(new Error('Get lyric failed'))
        }
        const msg = {
          data: {
            lrclist: lrcInfo.lrc
          },
          status: 200
        }
        return msg
      })
    })
    const asd = async () => {
      return await new Promise((resolve) => {
        requestObj.promise.then((re) => {
          resolve(re)
        })
      })
    }
    const as = await asd()
    return as;
    // return this.commonRequest(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${musicId}&httpsStatus=1`)
  }
}

6.进入 .eslintrc 文件,把下面代码替换(此替换不影响其他地方功能)

{
  "extends": ["eslint-config-egg/typescript","eslint-config-standard","eslint:recommended","plugin:jsdoc/recommended"],
  "parser": "@typescript-eslint/parser",
  "plugins": ["jsdoc"],
	"rules": {
		"comma-dangle":0,
		"operator-linebreak":["error","before"],
		"space-before-function-paren":0,
		"linebreak-style":0,
		"no-var-requires":0,
		"no-return-assign":"off",
		"default-case":"off",
		"no-useless-constructor":"off",
		"no-unused-vars":0,
		"jsdoc/require-param-description":"off",
		"jsdoc/check-tag-names": 0,
		"jsdoc/no-undefined-types":0,
		"jsdoc/valid-types":0,
		"jsdoc/tag-lines":0,
		"jsdoc/require-returns":0,
		"jsdoc/check-param-names":0,
		"no-bitwise":0,
		"no-confusing-arrow":0,
		"arrow-parens":0,
		"semi":0
	},
	"overrides":[
	 {
		"files":["*.ts"], 
		"rules":{
			"@typescript-eslint/no-unused-vars":0,
			"@typescript-eslint/semi":0,
			"@typescript-eslint/no-var-requires":0,
			"@typescript-eslint/no-useless-constructor":0
		}
	}
	],
	"globals": {
		"_":"readonly",
		"app":true
	},
	"env": {
		"node": true
	}
}

7.使用 npm run ci 编译成 js 代码

8.最后启动项目npm run start

9.可以进行歌词接口代替,使用桌面客户端解密歌词接口更稳定,更快。

@wifi-left
Copy link

感谢大佬~
本人改写了个php版本
也分享下:
kuwolrc.php

<?php
include("./libs.php");
// 定义strToHexBuffer函数,接受一个字符串参数
$get_id = 0;
if (empty($_GET['id'])) {
    http_response_code(403);
    echo '{"code":403,"msg":"Wrong params."}';
    return;
}
$get_id = $_GET['id'];
$bufkey = strToHexBuffer('yeelion');
$bufkeylen = count($bufkey);
// 调用函数,得到返回值(16进制的buffer数组)

function buildParams($id, $isGetLyrics = true)
{
    $params = "user=1,web,web,web&requester=localhost&req=1&rid=MUSIC_$id";
    if ($isGetLyrics) $params .= '&lrcx=1';
    $bufstr = strToHexBuffer($params);
    $bufstrlen = count($bufstr);
    $output = $bufstr;
    $bufkeylen = $GLOBALS['bufkeylen'];
    $bufkey = $GLOBALS['bufkey'];
    for ($i = 0; $i < $bufstrlen; $i++) {
        $output[$i] = 0;
    }
    $i = 0;
    while ($i < $bufstrlen) {
        $j = 0;
        while ($j < $bufkeylen && $i < $bufstrlen) {
            $output[$i] = $bufkey[$j] ^ $bufstr[$i];
            $i++;
            $j++;
        }
    }
    //print_r($output);
    $string = implode($output);

    // 将字符串用base64方式编码
    $encoded = base64_encode($string);
    return $encoded;
}
function decodeLyrics($base64, $isGetLyrics = true)
{
    $str = ($base64);
    // echo $str;
    // echo (substr($str,0,10) == "tp=content")?"true":"false";
    if (substr($str, 0, 10) != 'tp=content') return false;
    // $buf = strToHexBuffer($str);
    $newstr = substr($str, strpos($str, "\r\n\r\n") + 4);
    // echo $newstr;
    $unzipstr = base64_decode(gzuncompress($newstr));
    $bufStr = strToHexBuffer($unzipstr);
    $bufStrlen = count($bufStr);
    $output = $bufStr;
    $i = 0;
    $bufkeylen = $GLOBALS['bufkeylen'];
    $bufkey = $GLOBALS['bufkey'];
    while($i< $bufStrlen){
        $j = 0;
        while($j<$bufkeylen && $i<$bufStrlen){
            $output[$i] = $bufStr[$i] ^ $bufkey[$j];
            $i++;
            $j++;
        }
    }
    $result = implode($output);
    return detect_encoding($result,'utf8');
    // return true;
}
/**
 * @ string 需要转换的文字
 * @ encoding 目标编码
 **/
function detect_encoding($string, $encoding = 'gbk')
{
    $is_utf8 = preg_match('%^(?:[\x09\x0A\x0D\x20-\x7E]| [\xC2-\xDF][\x80-\xBF]| \xE0[\xA0-\xBF][\x80-\xBF] | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  | \xED[\x80-\x9F][\x80-\xBF] | \xF0[\x90-\xBF][\x80-\xBF]{2} | [\xF1-\xF3][\x80-\xBF]{3} | \xF4[\x80-\x8F][\x80-\xBF]{2} )*$%xs', $string);
    if ($is_utf8 && $encoding == 'utf8') {
        return $string;
    } elseif ($is_utf8) {
        return mb_convert_encoding($string, $encoding, "UTF-8");
    } else {
        return mb_convert_encoding($string, $encoding, 'gbk,gb2312,big5');
    }
}
$url = "http://newlyric.kuwo.cn/newlyric.lrc?" . buildParams($get_id);
//echo $url;
$data = fetchURL($url, false);
if ($data == false) {
    http_response_code(500);
    echo '{"msg":"获取失败 (Step#1)","code":500}';
}
$result = decodeLyrics($data);
if ($result == false) {
    http_response_code(500);
    echo '{"msg":"获取失败 (Step#2)","code":500}';
}
echo $result;

libs.php

function strToHexBuffer($str)
{
    // 将字符串转换为字节数组
    $bytes = unpack("C*", $str);
    // 定义输出数组,用于存储16进制的buffer元素
    $output = array();
    // 循环遍历字节数组,将每个字节转换为16进制,并添加到输出数组中
    foreach ($bytes as $byte) {
        // 使用sprintf函数格式化16进制,并在前面补0(如果需要)
        $hex = sprintf("%02x", $byte);
        // 使用pack函数将16进制转换为二进制,并添加到输出数组中
        $output[] = pack("H*", $hex);
    }
    // 返回输出数组
    return $output;
}
function fetchURL($url, $ispost = false, $postcontent = "")
{
    //echo $url;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 8);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_HTTPHEADER, array("Cache-Control: no-cache"));
    curl_setopt($ch, CURLOPT_COOKIE, "NMTID=00OTrNMnhnWaFznwESKkN7usch8O14AAAGDKs_klA;");
    if ($ispost) {
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postcontent);
    }
    $output = curl_exec($ch);
    // echo json_encode(curl_getinfo($ch));
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($output === false) {
        if (in_array(intval(curl_errno($ch)), [7, 28], true)) {
            echo '{"success":"fail","msg":"连接超时,请重试。","code":3}';
            return false;
            //超时的处理代码
        } else if (in_array(intval(curl_errno($ch)), [3], true)) {
            echo '{"success":"fail","msg":"C++ CURL 不支持的 URL:' . $url . '","code":4}';
        } else {
            echo '{"success":"fail","msg":"[' . curl_errno($ch) . '] ' . curl_strerror(curl_errno($ch)) . '; ' . curl_error($ch) . '","code":4}';
            return false;
        }
    }
    if ($httpCode >= 400 && $httpCode < 404) {
        echo '{"success":"fail","msg":"无法访问该文件,请联系站点管理员。","code":5}';
        return false;
    } else if ($httpCode >= 404 && $httpCode < 500) {
        echo '{"success":"fail","msg":"服务器无法找到文件。","code":5}';
        return false;
    } else if ($httpCode >= 500) {
        echo '{"success":"fail","msg":"服务器发生错误,请稍后重试。(Http Status:' . $httpCode . ')","code":6}';
        return false;
    }
    curl_close($ch);
    return $output;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants