[
  {
    "path": ".dockerignore",
    "content": ".git\n.npmignore\n.gitignore\n.dockerignore\n\nLICENSE\n*.md\n\nnode_modules\nnpm-debug.log\n\nDockerfile*\n*.yml\n\nsrc/browser/\nca.*\n*.worker.js"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\n\non:\n  push:\n    tags:\n       - '*'\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    env:\n      REPOSITORY: unblockneteasemusic\n      DOCKER_USERNAME: nondanee\n      DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}\n    steps:\n      -\n        name: Prepare\n        id: prepare\n        run: |\n          ARCH=(amd64 arm/v6 arm/v7 arm64 386 ppc64le s390x)\n          PLATFORM=$(printf \",linux/%s\" \"${ARCH[@]}\")\n          echo ::set-output name=build_platform::${PLATFORM:1}\n          echo ::set-output name=image_name::${DOCKER_USERNAME}/${REPOSITORY}\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Setup Buildx\n        uses: crazy-max/ghaction-docker-buildx@v1\n      -\n        name: Login\n        run: |\n          echo \"${DOCKER_PASSWORD}\" | docker login --username ${DOCKER_USERNAME} --password-stdin\n      -\n        name: Build\n        run: |\n          docker buildx build \\\n            --tag ${{ steps.prepare.outputs.image_name }} \\\n            --platform ${{ steps.prepare.outputs.build_platform }} \\\n            --output \"type=image,push=true\" \\\n            --file Dockerfile .\n      -\n        name: Check Manifest\n        run: |\n          docker buildx imagetools inspect ${{ steps.prepare.outputs.image_name }}\n\n  npm:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Setup Node.js\n        uses: actions/setup-node@v1\n        with:\n          registry-url: https://registry.npmjs.org/\n      -\n        name: Publish\n        run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# IDE\n.vscode\n.idea\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# pkg dist directory\ndist/\n\n# es6 transformation\nsrc/browser/provider\nsrc/browser/cache.js"
  },
  {
    "path": ".npmignore",
    "content": ".npmignore\n.gitignore\n.dockerignore\n\nDockerfile*\n*.yml\n\nsrc/browser/\nca.*\n*.worker.js"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine\nRUN apk add --update nodejs npm --repository=http://dl-cdn.alpinelinux.org/alpine/latest-stable/main/\n\nENV NODE_ENV production\n\nWORKDIR /usr/src/app\nCOPY package*.json ./\nRUN npm install --production\nCOPY . .\n\nEXPOSE 8080 8081\n\nENTRYPOINT [\"node\", \"app.js\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Nzix\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://user-images.githubusercontent.com/26399680/47980314-0e3f1700-e102-11e8-8857-e3436ecc8beb.png\" alt=\"logo\" width=\"140\" height=\"140\" align=\"right\">\n\n# UnblockNeteaseMusic\n\n解锁网易云音乐客户端变灰歌曲\n\n## 特性\n\n- 使用 QQ / 虾米 / 百度 / 酷狗 / 酷我 / 咪咕 / JOOX 音源替换变灰歌曲链接 (默认仅启用一、五、六)\n- 为请求增加 `X-Real-IP` 参数解锁海外限制，支持指定网易云服务器 IP，支持设置上游 HTTP / HTTPS 代理\n- 完整的流量代理功能 (HTTP / HTTPS)，可直接作为系统代理 (同时支持 PAC)\n\n## 运行\n\n使用 npx\n\n```\n$ npx @nondanee/unblockneteasemusic\n```\n\n或使用 Docker\n\n```\n$ docker run nondanee/unblockneteasemusic\n```\n\n```\n$ docker-compose up\n```\n\n### 配置参数\n\n```\n$ unblockneteasemusic -h\nusage: unblockneteasemusic [-v] [-p port] [-a address] [-u url] [-f host]\n                           [-o source [source ...]] [-t token] [-e url] [-s]\n                           [-h]\n\noptional arguments:\n  -v, --version                   output the version number\n  -p port, --port port            specify server port\n  -a address, --address address   specify server host\n  -u url, --proxy-url url         request through upstream proxy\n  -f host, --force-host host      force the netease server ip\n  -o source [source ...], --match-order source [source ...]\n                                  set priority of sources\n  -t token, --token token         set up proxy authentication\n  -e url, --endpoint url          replace virtual endpoint with public host\n  -s, --strict                    enable proxy limitation\n  -h, --help                      output usage information\n```\n\n## 使用\n\n**警告：本项目不提供线上 demo，请不要轻易信任使用他人提供的公开代理服务，以免发生安全问题**\n\n**若将服务部署到公网，强烈建议使用严格模式 (此模式下仅放行网易云音乐所属域名的请求) `-s`  限制代理范围 (需使用 PAC 或 hosts)，~~或启用 Proxy Authentication `-t <name>:<password>` 设置代理用户名密码~~ (目前密码认证在 Windows 客户端设置和 macOS 系统设置都无法生效，请不要使用)，以防代理被他人滥用**\n\n支持 Windows 客户端，UWP 客户端，Android 客户端，Linux 客户端 (1.2 版本以上需要自签证书 MITM，启动客户端需要增加 `--ignore-certificate-errors` 参数)，macOS 客户端 (726 版本以上需要自签证书)，iOS 客户端 (配置 https endpoint 或使用自签证书) 和网页版 (需要自签证书，需要脚本配合)\n\n目前除 UWP 外其它客户端均优先请求 HTTPS 接口，默认配置下本代理对网易云所有 HTTPS API 连接返回空数据，促使客户端降级使用 HTTP 接口 (新版 Linux 客户端和 macOS 客户端已无法降级)\n\n因 UWP 应用存在网络隔离，限制流量发送到本机，若使用的代理在 localhost，或修改的 hosts 指向 localhost，需为 \"网易云音乐 UWP\" 手动开启 loopback 才能使用，请以**管理员身份**执行命令\n\n```powershell\nchecknetisolation loopbackexempt -a -n=\"1F8B0F94.122165AE053F_j2p0p5q0044a6\"\n```\n\n### 方法 1. 修改 hosts\n\n向 hosts 文件添加两条规则\n\n```\n<Server IP> music.163.com\n<Server IP> interface.music.163.com\n```\n\n> 使用此方法必须监听 80 端口 `-p 80` \n>\n> **若在本机运行程序**，请指定网易云服务器 IP `-f xxx.xxx.xxx.xxx` (可在修改 hosts 前通过 `ping music.163.com` 获得) **或** 使用代理 `-u http(s)://xxx.xxx.xxx.xxx:xxx`，以防请求死循环\n>\n> **Android 客户端下修改 hosts 无法直接使用**，原因和解决方法详见[云音乐安卓又搞事啦](https://jixun.moe/post/netease-android-hosts-bypass/)，[安卓免 root 绕过网易云音乐 IP 限制](https://jixun.moe/post/android-block-netease-without-root/)\n\n### 方法 2. 设置代理\n\nPAC 自动代理脚本地址 `http://<Server Name:PORT>/proxy.pac`\n\n全局代理地址填写服务器地址和端口号即可\n\n| 平台    | 基础设置 |\n| :------ | :------------------------------- |\n| Windows | 设置 > 工具 > 自定义代理 (客户端内) |\n| UWP     | Windows 设置 > 网络和 Internet > 代理 |\n| Linux   | 系统设置 > 网络 > 网络代理 |\n| macOS   | 系统偏好设置 > 网络 > 高级 > 代理 |\n| Android | WLAN > 修改网络 > 高级选项 > 代理 |\n| iOS     | 无线局域网 > HTTP 代理 > 配置代理 |\n\n> 代理工具和方法有很多请自行探索，欢迎在 issues 讨论\n\n### ✳方法 3. 调用接口\n\n作为依赖库使用\n\n```\n$ npm install @nondanee/unblockneteasemusic\n```\n\n```javascript\nconst match = require('@nondanee/unblockneteasemusic')\n\n/** \n * Set proxy or hosts if needed\n */\nglobal.proxy = require('url').parse('http://127.0.0.1:1080')\nglobal.hosts = {'i.y.qq.com': '59.37.96.220'}\n\n/**\n * Find matching song from other platforms\n * @param {Number} id netease song id\n * @param {Array<String>||undefined} source support qq, xiami, baidu, kugou, kuwo, migu, joox\n * @return {Promise<Object>}\n */\nmatch(418602084, ['qq', 'kuwo', 'migu']).then(console.log)\n```\n\n## 效果\n\n#### Windows 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/60316017-87de8a80-999b-11e9-9381-16d40efbe7f6.png\" width=\"100%\">\n\n#### UWP 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/52215123-5a028780-28ce-11e9-8491-08c4c5dac3b4.png\" width=\"100%\">\n\n#### Linux 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/60316169-18b56600-999c-11e9-8ae5-5cd168b0edae.png\" width=\"100%\">\n\n#### macOS 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/52196035-51418f80-2895-11e9-8f33-78a631cdf151.png\" width=\"100%\">\n\n#### Android 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/57972549-eabd2900-79ce-11e9-8fef-95cb60906298.png\" width=\"50%\">\n\n#### iOS 客户端\n\n<img src=\"https://user-images.githubusercontent.com/26399680/57972440-f90a4580-79cc-11e9-8dbf-6150ee299b9c.jpg\" width=\"50%\">\n\n## 致谢\n\n感谢大佬们为逆向 eapi 所做的努力\n\n使用的其它平台音源 API 出自\n\n[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)\n\n[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)\n\n向所有同类项目致敬\n\n[EraserKing/CloudMusicGear](https://github.com/EraserKing/CloudMusicGear)\n\n[EraserKing/Unblock163MusicClient](https://github.com/EraserKing/Unblock163MusicClient)\n\n[ITJesse/UnblockNeteaseMusic](https://github.com/ITJesse/UnblockNeteaseMusic/)\n\n[bin456789/Unblock163MusicClient-Xposed](https://github.com/bin456789/Unblock163MusicClient-Xposed)\n\n[YiuChoi/Unlock163Music](https://github.com/YiuChoi/Unlock163Music)\n\n[yi-ji/NeteaseMusicAbroad](https://github.com/yi-ji/NeteaseMusicAbroad)\n\n[stomakun/NeteaseReverseLadder](https://github.com/stomakun/NeteaseReverseLadder/)\n\n[fengjueming/unblock-NetEaseMusic](https://github.com/fengjueming/unblock-NetEaseMusic)\n\n[acgotaku/NetEaseMusicWorld](https://github.com/acgotaku/NetEaseMusicWorld)\n\n[mengskysama/163-Cloud-Music-Unlock](https://github.com/mengskysama/163-Cloud-Music-Unlock)\n\n[azureplus/163-music-unlock](https://github.com/azureplus/163-music-unlock)\n\n[typcn/163music-mac-client-unlock](https://github.com/typcn/163music-mac-client-unlock)\n\n## 许可\n\nThe MIT License"
  },
  {
    "path": "app.js",
    "content": "#!/usr/bin/env node\nrequire('./src/app')"
  },
  {
    "path": "bridge.js",
    "content": "#!/usr/bin/env node\nrequire('./src/bridge')"
  },
  {
    "path": "ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIJAKX8LdIETDklMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb\nBgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTE5MDUxODE2MDU0NVoXDTI0\nMDUxNjE2MDU0NVowUjELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRl\nYXNlTXVzaWMgUm9vdCBDQTEdMBsGA1UECgwUR2l0SHViLmNvbSBAbm9uZGFuZWUw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD23K6Ti2TfLJToCmpCAVgE\nXb8+qTMfrifCpnKlJ+hrL+4KI1j4vSqTOOatqmxGSXZdF/j2kJuI40YThaokcgYx\nGFcPcEftSCYGWy8o20u2hzTkkW3KW9wlsDRIXICFXVIsHeSDwz+aVSudkyJHjfaS\naLNb5pPovE7MRj8tDbp55scaSqhEcOe3m1ZlwlCeeXvD7RLKr3xhBKbGEqlJAjFq\nRNGzuqylqyJVBLScNHC7Lcf4n6pKr1yPGOeLePOUrIwtj0ynHUcBfeMuCVCsIKL8\nvy/oNwlDrZaAMfu5QQslzEf87KY1QgtI6Ppii+tzbmVx1ZxnlaCKqiuwlgBoi/5r\nAgMBAAGjUDBOMB0GA1UdDgQWBBRDhbGjnXEUouE9wNFS2k9PtgYYjDAfBgNVHSME\nGDAWgBRDhbGjnXEUouE9wNFS2k9PtgYYjDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBCwUAA4IBAQDRUh5+JFLEALXQkhPfwrVf4sCXTwLMwVujTPo3NMbhpWiP4cnn\nXHGCD5V57bBwjeD6NSrczDIdnN9uTJyFmLNVFMZBguEIeZfLUJLJ6w1ZhfgciX1D\n9djyyo6eclkHvi+aPZKfzgMmc5BvHcjyUyS5MzI23kUW6WXUDn3IDIUKrfaH9Mjc\n/d4DDZVKQCYrLoBL+XO7pEHUY0u9XZVYWEavQ5tSN8XY1SDrO0yGUpRWET0ltubE\nzV7W0LOhuoVCiemboc5H8+njBjCis8obAo1XMmDZzW189L9GPFxHNWlka+KlajZB\ntMo90PooZYEOw1rTUrzHb+VZY/tYIAAomGZ0\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  unblockneteasemusic:\n    image: nondanee/unblockneteasemusic\n    environment:\n      NODE_ENV: production\n    ports:\n      - 8080:8080\n"
  },
  {
    "path": "endpoint.worker.js",
    "content": "addEventListener('fetch', event => {\n  event.respondWith(handleRequest(event.request))\n})\n\nconst pattern = /^\\/package\\/([0-9a-zA-Z_\\-=]+)\\/(\\w+\\.\\w+)$/\n\nconst handleRequest = async request => {\n  const notFound = new Response(null, { status: 404 })\n  const path = new URL(request.url).pathname\n  const [matched, base64Url, fileName] = pattern.exec(path || '') || []\n  if (!matched) return notFound\n  let url = base64Url.replace(/-/g, '+').replace(/_/g, '/')\n  try { url = new URL(atob(url)) } catch(_) { url = null }\n  if (!url) return notFound\n  const headers = new Headers(request.headers)\n  headers.set('host', url.host)\n  headers.delete('cookie')\n  const { method, body } = request\n  return fetch(url, { method, headers, body })\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@nondanee/unblockneteasemusic\",\n  \"version\": \"0.25.3\",\n  \"description\": \"Revive unavailable songs for Netease Cloud Music\",\n  \"main\": \"src/provider/match.js\",\n  \"bin\": {\n    \"unblockneteasemusic\": \"app.js\"\n  },\n  \"scripts\": {\n    \"pkg\": \"pkg . --out-path=dist/\"\n  },\n  \"pkg\": {\n    \"assets\": [\n      \"server.key\",\n      \"server.crt\"\n    ]\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nondanee/UnblockNeteaseMusic.git\"\n  },\n  \"author\": \"nondanee\",\n  \"license\": \"MIT\",\n  \"dependencies\": {},\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDkjCCAnqgAwIBAgIJAK/bIUIlE36LMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb\nBgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTIwMDUxNjE3MTIxM1oXDTIx\nMDUxNjE3MTIxM1owezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSww\nKgYDVQQKDCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8G\nA1UECwwISVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALobECypwEoe8VqM/FJvBRR3p2T+ZWdi\nMSPrwfiRJr5p7OMtWBlLveCBV85+R3feidYbQTXlvVTdToY+GN6mFE1x6zG2dvLD\ns4UuRnipmvGcFYhIRTX8J4AJiN8VMtW0TNXscRMudpz/FAVtsRrggRaThYg4f/rI\noAPMqKMsS4JoYhxs9ED6E6/tpj3XmSg1ekaXhgacYSYHeyxizZwoOFVCLH3TG5sF\nsD6CYNnukYol8bR+VRpvHftIYss5Yz+DyyhYEAMJm1CfQo+xoGR3D0ozbT3hUnzm\nfEoOhmSp3sALrFVE4iJSuajoh2/3xhmcyi3xZdWyq4F8hpb+URyaoW0CAwEAAaNC\nMEAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwKQYDVR0RBCIwIIINbXVzaWMuMTYzLmNv\nbYIPKi5tdXNpYy4xNjMuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQB+zW0o1169aGQI\n7GA/8BQ769svpkpdy/lfvkokapFjzoxLTBQhjMo9rqzmGOwr9ksePwQqSDXn685W\nmKnEl0CzhBrKnL5x3gHus8bg591xpW+01xAFXSyLITOfMJqMEdY7ylymkm0XZ3aN\nvm+yFdP1fr/bZNw6Wrprg3i7eGhj7TdBXRA96usVgBcnCkC1SzEZfnDZsKl9o8Xx\nTSOpvzIMSaD7++Bp7BdzA5oCCydv2c++zV/sgCPIr26Jq8UQac+qQP5SMlYyGbAl\nvIQRRZyfQ4fPonYDnEPHWFCMyBkQIN39LMhDRsUgn8bT0rnP91xkNAd9S4VWbNDA\n5TMiQy3F\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuhsQLKnASh7xWoz8Um8FFHenZP5lZ2IxI+vB+JEmvmns4y1Y\nGUu94IFXzn5Hd96J1htBNeW9VN1Ohj4Y3qYUTXHrMbZ28sOzhS5GeKma8ZwViEhF\nNfwngAmI3xUy1bRM1exxEy52nP8UBW2xGuCBFpOFiDh/+sigA8yooyxLgmhiHGz0\nQPoTr+2mPdeZKDV6RpeGBpxhJgd7LGLNnCg4VUIsfdMbmwWwPoJg2e6RiiXxtH5V\nGm8d+0hiyzljP4PLKFgQAwmbUJ9Cj7GgZHcPSjNtPeFSfOZ8Sg6GZKnewAusVUTi\nIlK5qOiHb/fGGZzKLfFl1bKrgXyGlv5RHJqhbQIDAQABAoIBAEmAvtalBMlBh1mY\nLV/xcTQwPfDpeOtoILhrOOUPjxnNhD4FfrIe9BNjgmaQAXIadp4VjZ/X6PtHnOfw\nRqpJNeOQhq/PvRMMsC59pF+rvQKH/wkgYhV8Ta2IFoLlQHqfB2nGRLKquzYumJ28\nQSK4YMOl6CtxBTrrWiemAUTRDdGm8tARiipJH1SEJrS6d/NoRoJx2vixFgD2eS6X\nbjnhGjIzvX/w5FWjctqj+SFITP1UI62b6DyWsPOkoosKNteK+Ulz+K6ZFvOx7day\nXgUoTcVpwCVr2dVGhJtOrbKPcl1jYCYHJAHwzUZND4x4yftm1mnnsi3bthYqbtHQ\nvxLE9YECgYEA9hiZxwiVvLjSe1xT/D75HbB8S1XSnwzpMmqgzStguxCQ0Qg5yiLI\nUKRDY8UZvEDV4i2bQGy7mk8lFvX1q2z7Q30+dtT9r2N9a6ujMk5RMfo2BZg/poI6\nyDWe2tKUg9cTwfgni4TutLOYkpz3VDPIQHs3k2mpNh7f+8X4RIybDqkCgYEAwZhp\nuWMV38Bb0WytswHXL1dRuwBskKqALUBY61dtXkyBuocj8AuRRxfxfZpgJRrHFxDX\nO9bQ2nxpVlwKsR6DJDUdxU3+kvwyPfseU5XUBey8WdkuAKD7cKZOHMhFVWccks0U\nYJzykNrxB+rGTiwVKa0MOhipuJ7boerwwaN2SyUCgYBP9Ow5o4tq9q3EUNoksZ0k\nzUuE+oxlCr/VlplKL9bM0HQMxlxoVWa59LTEfKyA4pvbUbAIfYtydlZ5oE5CdTUp\n105tM4R88Jk2W1y5ooJ093OH29CKW/OXSvyi4hpIv592vRa0GOupoFRpBkDBhdWB\nRcdnyMOmht+FIOwp8XkLiQKBgAUK3j4Y6ZnxXbLfvMp70soF4TgYs7s05a/IDEjc\n9xlMrthX6sS22GrcocqeucBdqS/dnW2Ok9QNB4VbUl/4pnvL8mGQPYBAl2Jr5wdQ\nULxyxRkmAf+8MbBmdIRlZwDpdaIRO2Wk0OCbA0osgEvK9CYovrfIqqsHYDsgbnLs\nugkNAoGBAJok06BN05caPXXLQ2pMwI/7mjcZFjcOMxSloYi7LFkxlyvoTqReAeSa\nyOb6W/7obS1X8ms/EAkqiyzJuPtNZJCW/nvV0iCoZ/NxLuyHnFaO344GBAweol+S\nJx0MY8KuDCyeGErc2xdz/yr3ld2PSTq71dhBluGyba2YX+peJ2Yv\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "src/app.js",
    "content": "#!/usr/bin/env node\n\nconst package = require('../package.json')\nconst config = require('./cli.js')\n.program({name: package.name.replace(/@.+\\//, ''), version: package.version})\n.option(['-v', '--version'], {action: 'version'})\n.option(['-p', '--port'], {metavar: 'port', help: 'specify server port'})\n.option(['-a', '--address'], {metavar: 'address', help: 'specify server host'})\n.option(['-u', '--proxy-url'], {metavar: 'url', help: 'request through upstream proxy'})\n.option(['-f', '--force-host'], {metavar: 'host', help: 'force the netease server ip'})\n.option(['-o', '--match-order'], {metavar: 'source', nargs: '+', help: 'set priority of sources'})\n.option(['-t', '--token'], {metavar: 'token', help: 'set up proxy authentication'})\n.option(['-e', '--endpoint'], {metavar: 'url', help: 'replace virtual endpoint with public host'})\n.option(['-s', '--strict'], {action: 'store_true', help: 'enable proxy limitation'})\n.option(['-h', '--help'], {action: 'help'})\n.parse(process.argv)\n\nglobal.address = config.address\nconfig.port = (config.port || '8080').split(':').map(string => parseInt(string))\nconst invalid = value => (isNaN(value) || value < 1 || value > 65535)\nif (config.port.some(invalid)) {\n\tconsole.log('Port must be a number higher than 0 and lower than 65535.')\n\tprocess.exit(1)\n}\nif (config.proxyUrl && !/http(s?):\\/\\/.+:\\d+/.test(config.proxyUrl)) {\n\tconsole.log('Please check the proxy url.')\n\tprocess.exit(1)\n}\nif (config.endpoint && !/http(s?):\\/\\/.+/.test(config.endpoint)) {\n\tconsole.log('Please check the endpoint host.')\n\tprocess.exit(1)\n}\nif (config.forceHost && require('net').isIP(config.forceHost) === 0) {\n\tconsole.log('Please check the server host.')\n\tprocess.exit(1)\n}\nif (config.matchOrder) {\n\tconst provider = new Set(['netease', 'qq', 'xiami', 'baidu', 'kugou', 'kuwo', 'migu', 'joox', 'youtube'])\n\tconst candidate = config.matchOrder\n\tif (candidate.some((key, index) => index != candidate.indexOf(key))) {\n\t\tconsole.log('Please check the duplication in match order.')\n\t\tprocess.exit(1)\n\t}\n\telse if (candidate.some(key => !provider.has(key))) {\n\t\tconsole.log('Please check the availability of match sources.')\n\t\tprocess.exit(1)\n\t}\n\tglobal.source = candidate\n}\nif (config.token && !/\\S+:\\S+/.test(config.token)) {\n\tconsole.log('Please check the authentication token.')\n\tprocess.exit(1)\n}\n\nconst parse = require('url').parse\nconst hook = require('./hook')\nconst server = require('./server')\nconst random = array => array[Math.floor(Math.random() * array.length)]\nconst target = Array.from(hook.target.host)\n\nglobal.port = config.port\nglobal.proxy = config.proxyUrl ? parse(config.proxyUrl) : null\nglobal.hosts = target.reduce((result, host) => Object.assign(result, {[host]: config.forceHost}), {})\nserver.whitelist = ['://[\\\\w.]*music\\\\.126\\\\.net', '://[\\\\w.]*vod\\\\.126\\\\.net']\nif (config.strict) server.blacklist.push('.*')\nserver.authentication = config.token || null\nglobal.endpoint = config.endpoint\nif (config.endpoint) server.whitelist.push(escape(config.endpoint))\n\n// hosts['music.httpdns.c.163.com'] = random(['59.111.181.35', '59.111.181.38'])\n// hosts['httpdns.n.netease.com'] = random(['59.111.179.213', '59.111.179.214'])\n\nconst dns = host => new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error ? reject(error) : resolve(records.map(record => record.address))))\nconst httpdns = host => require('./request')('POST', 'http://music.httpdns.c.163.com/d', {}, host).then(response => response.json()).then(jsonBody => jsonBody.dns.reduce((result, domain) => result.concat(domain.ips), []))\nconst httpdns2 = host => require('./request')('GET', 'http://httpdns.n.netease.com/httpdns/v2/d?domain=' + host).then(response => response.json()).then(jsonBody => Object.keys(jsonBody.data).map(key => jsonBody.data[key]).reduce((result, value) => result.concat(value.ip || []), []))\n\nPromise.all([httpdns, httpdns2].map(query => query(target.join(','))).concat(target.map(dns)))\n.then(result => {\n\tconst {host} = hook.target\n\tresult.forEach(array => array.forEach(host.add, host))\n\tserver.whitelist = server.whitelist.concat(Array.from(host).map(escape))\n\tconst log = type => console.log(`${['HTTP', 'HTTPS'][type]} Server running @ http://${address || '0.0.0.0'}:${port[type]}`)\n\tif (port[0]) server.http.listen(port[0], address).once('listening', () => log(0))\n\tif (port[1]) server.https.listen(port[1], address).once('listening', () => log(1))\n})\n.catch(error => console.log(error))\n"
  },
  {
    "path": "src/bridge.js",
    "content": "#!/usr/bin/env node\nconst cache = require('./cache')\nconst parse = require('url').parse\nrequire('./provider/insure').disable = true\n\nconst router = {\n\tqq: require('./provider/qq'),\n\txiami: require('./provider/xiami'),\n\tbaidu: require('./provider/baidu'),\n\tkugou: require('./provider/kugou'),\n\tkuwo: require('./provider/kuwo'),\n\tmigu: require('./provider/migu'),\n\tjoox: require('./provider/joox')\n}\n\nconst distribute = (url, router) =>\n\tPromise.resolve()\n\t.then(() => {\n\t\tconst route = url.pathname.slice(1).split('/').map(path => decodeURIComponent(path))\n\t\tlet pointer = router, argument = decodeURIComponent(url.query)\n\t\ttry {argument = JSON.parse(argument)} catch(e) {}\n\t\tconst miss = route.some(path => {\n\t\t\tif (path in pointer) pointer = pointer[path]\n\t\t\telse return true\n\t\t})\n\t\tif (miss || typeof pointer != 'function') return Promise.reject()\n\t\t// return pointer.call(null, argument)\n\t\treturn cache(pointer, argument, 15 * 60 * 1000)\n\t})\n\nrequire('http').createServer()\n.listen(parseInt(process.argv[2]) || 9000)\n.on('request', (req, res) =>\n\tdistribute(parse(req.url), router)\n\t.then(data => res.write(data))\n\t.catch(() => res.writeHead(404))\n\t.then(() => res.end())\n)"
  },
  {
    "path": "src/browser/README.md",
    "content": "# Web Extension Port\n\nFor test\n\n## Implementation\n\n- Convert node module to ES6 module which can be directly executed in Chrome 61+ without Babel\n- Rewrite crypto module (using CryptoJS) and request (using XMLHttpRequest) module for browser environment\n- Do matching in background and transfer result with chrome runtime communication\n- Inject content script for hijacking Netease Music Web Ajax response\n\n## Build\n\n```\n$ node convert.js\n```\n\n## Install\n\nLoad unpacked extension in Developer mode\n\n## Known Issue\n\nAudio resources from `kuwo`, `kugou` and `migu` are limited in http protocol only and hence can't load\nMost audio resources from `qq` don't support preflight request (OPTIONS) and make playbar buggy\n\n## Reference\n\n- [brix/crypto-js](https://github.com/brix/crypto-js)\n- [travist/jsencrypt](https://github.com/travist/jsencrypt)\n- [JixunMoe/cuwcl4c](https://github.com/JixunMoe/cuwcl4c)"
  },
  {
    "path": "src/browser/background.html",
    "content": "<script src=\"https://cdn.jsdelivr.net/npm/crypto-js/crypto-js.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/jsencrypt/bin/jsencrypt.min.js\"></script>\n<script type=\"module\" src=\"background.js\"></script>"
  },
  {
    "path": "src/browser/background.js",
    "content": "import match from './provider/match.js'\nconst self = chrome.runtime.id\n\nchrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {\n\tmatch(request.match, ['qq'])\n\t.then(song => sendResponse(song))\n\t.catch(console.log)\n\treturn true\n})\n\nchrome.webRequest.onBeforeSendHeaders.addListener(details => {\n\tlet headers = details.requestHeaders\n\tif(details.url.includes('//music.163.com/')){\n\t\theaders.push({name: 'X-Real-IP', value: '118.88.88.88'})\n\t}\n\tif(details.initiator == `chrome-extension://${self}`){\n\t\tlet index = headers.findIndex(item => item.name.toLowerCase() === 'additional-headers')\n\t\tif(index === -1) return\n\t\tObject.entries(JSON.parse(atob(headers[index].value))).forEach(entry => headers.push({name: entry[0], value: entry[1]}))\n\t\theaders.splice(index, 1)\n\t}\n\tif(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){\n\t\theaders = headers.filter(item => !['referer', 'origin'].includes(item.name.toLowerCase()))\n\t}\n\treturn {requestHeaders: headers}\n}, {urls: ['*://*/*']}, ['blocking', 'requestHeaders', 'extraHeaders'])\n\nchrome.webRequest.onHeadersReceived.addListener(details => {\n\tlet headers = details.responseHeaders\n\tif(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){\n\t\theaders.push({name: 'Access-Control-Allow-Origin', value: '*'})\n\t}\n\treturn {responseHeaders: headers}\n}, {urls: ['*://*/*']}, ['blocking', 'responseHeaders'])"
  },
  {
    "path": "src/browser/convert.js",
    "content": "const fs = require('fs')\nconst path = require('path')\n\nconst importReplacer = (match, state, alias, file) => {\n\tfile = file + (file.endsWith('.js') ? '' : '.js')\n\treturn `import ${alias} from '${file}'`\n}\n\nconst converter = (input, output, processor) => {\n\tlet data = fs.readFileSync(input).toString()\n\tif(processor){\n\t\tdata = processor(data)\n\t}\n\telse{\n\t\tdata = data.replace(/global\\./g, 'window.')\n\t\tdata = data.replace(/(const|let|var)\\s+(\\w+)\\s*=\\s*require\\(\\s*['|\"](.+)['|\"]\\s*\\)/g, importReplacer)\n\t\tdata = data.replace(/module\\.exports\\s*=\\s*/g, 'export default ')\n\t}\n\tfs.writeFileSync(output, data)\n}\n\nconverter(path.resolve(__dirname, '..', 'cache.js'), path.resolve(__dirname, '.', 'cache.js'))\n\nif(!fs.existsSync(path.resolve(__dirname, 'provider'))) fs.mkdirSync(path.resolve(__dirname, 'provider'))\n\nfs.readdirSync(path.resolve(__dirname, '..', 'provider')).filter(file => !file.includes('test')).forEach(file => {\n\tconverter(path.resolve(__dirname, '..', 'provider', file), path.resolve(__dirname, 'provider', file))\n})\n\nconst providerReplacer = (match, state, data) => {\n\tlet provider = []\n\tlet imports = data.match(/\\w+\\s*:\\s*require\\(['|\"].+['|\"]\\)/g).map(line => {\n\t\tline = line.match(/(\\w+)\\s*:\\s*require\\(['|\"](.+)['|\"]\\)/)\n\t\tprovider.push(line[1])\n\t\treturn importReplacer(null, null, line[1], line[2])\n\t})\n\treturn imports.join('\\n') + '\\n\\n' + `${state} provider = {${provider.join(', ')}}`\n}\n\nconverter(path.resolve(__dirname, 'provider', 'match.js'), path.resolve(__dirname, 'provider', 'match.js'), data => {\n\tdata = data.replace(/(const|let|var)\\s+provider\\s*=\\s*{([^}]+)}/g, providerReplacer)\n\treturn data\n})"
  },
  {
    "path": "src/browser/crypto.js",
    "content": "\nconst bodyify = object => Object.entries(object).map(entry => entry.map(encodeURIComponent).join('=')).join('&')\n\nconst toBuffer = string => (new TextEncoder()).encode(string)\nconst toHex = arrayBuffer => Array.from(arrayBuffer).map(n => n.toString(16).padStart(2, '0')).join('')\nconst toBase64 = arrayBuffer => btoa(arrayBuffer)\n\nexport default {\n\turi: {\n\t\tretrieve: id => {\n\t\t\tid = id.toString().trim()\n\t\t\tconst key = '3go8&$8*3*3h0k(2)2'\n\t\t\tlet string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('')\n\t\t\tlet result = CryptoJS.MD5(string).toString(CryptoJS.enc.Base64).replace(/\\//g, '_').replace(/\\+/g, '-')\n\t\t\treturn `http://p1.music.126.net/${result}/${id}`\n\t\t}\n\t},\n\tmd5: {\n\t\tdigest: value => CryptoJS.MD5(value).toString()\n\t},\n\tmiguapi: {\n\t\tencrypt: object => {\n\t\t\tlet text = JSON.stringify(object), signer = new JSEncrypt()\n\t\t\tlet password = Array.from(window.crypto.getRandomValues(new Uint8Array(32))).map(n => n.toString(16).padStart(2, '0')).join('')\n\t\t\tsigner.setPublicKey('-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\\n-----END PUBLIC KEY-----')\n\t\t\treturn bodyify({\n\t\t\t\tdata: CryptoJS.AES.encrypt(text, password).toString(),\n\t\t\t\tsecKey: signer.encrypt(password)\n\t\t\t})\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/browser/inject.js",
    "content": "(() => {\n\tconst remote = 'oleomikdicccalekkpcbfgdmpjehnpkp'\n\tconst remoteMatch = id => new Promise(resolve => {\n\t\tchrome.runtime.sendMessage(remote, {match: id}, response => {\n\t\t\tresolve(response)\n\t\t})\n\t})\n\n\tconst waitTimeout = wait => new Promise(resolve => {\n\t\tsetTimeout(() => {\n\t\t\tresolve()\n\t\t}, wait)\n\t})\n\n\tconst searchFunction = (object, keyword) =>\n\t\tObject.keys(object)\n\t\t.filter(key => object[key] && typeof object[key] == 'function')\n\t\t.find(key => String(object[key]).match(keyword))\n\n\tif(self.frameElement && self.frameElement.tagName == 'IFRAME'){ //in iframe\n\t\tconst keyOne = searchFunction(window.nej.e, '\\\\.dataset;if')\n\t\tconst keyTwo = searchFunction(window.nm.x, '\\\\.copyrightId==')\n\t\tconst keyThree = searchFunction(window.nm.x, '\\\\.privilege;if')\n\t\tconst functionOne = window.nej.e[keyOne]\n\n\t\twindow.nej.e[keyOne] = (z, name) => {\n\t\t\tif (name == 'copyright' || name == 'resCopyright') return 1\n\t\t\treturn functionOne(z, name)\n\t\t}\n\t\twindow.nm.x[keyTwo] = () => false\n\t\twindow.nm.x[keyThree] = song => {\n\t\t\tsong.status = 0\n\t\t\tif (song.privilege) song.privilege.pl = 320000\n\t\t\treturn 0\n\t\t}\n\t\tconst table = document.querySelector('table tbody')\n\t\tif(table) Array.from(table.childNodes)\n\t\t.filter(element => element.classList.contains('js-dis'))\n\t\t.forEach(element => element.classList.remove('js-dis'))\n\t}\n\telse{\n\t\tconst keyAjax = searchFunction(window.nej.j, '\\\\.replace\\\\(\"api\",\"weapi')\n\t\tconst functionAjax = window.nej.j[keyAjax]\n\t\twindow.nej.j[keyAjax] = (url, param) => {\n\t\t\tconst onload = param.onload\n\t\t\tparam.onload = data => {\n\t\t\t\tPromise.resolve()\n\t\t\t\t.then(() => {\n\t\t\t\t\tif(url.includes('enhance/player/url')){\n\t\t\t\t\t\tif(data.data[0].url){\n\t\t\t\t\t\t\tdata.data[0].url = data.data[0].url.replace(/(m\\d+?)(?!c)\\.music\\.126\\.net/, '$1c.music.126.net')\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse{\n\t\t\t\t\t\t\treturn Promise.race([remoteMatch(data.data[0].id), waitTimeout(4000)])\n\t\t\t\t\t\t\t.then(result => {\n\t\t\t\t\t\t\t\tif(result){\n\t\t\t\t\t\t\t\t\tdata.data[0].code = 200\n\t\t\t\t\t\t\t\t\tdata.data[0].br = 320000\n\t\t\t\t\t\t\t\t\tdata.data[0].type = 'mp3'\n\t\t\t\t\t\t\t\t\tdata.data[0].size = result.size\n\t\t\t\t\t\t\t\t\tdata.data[0].md5 = result.md5\n\t\t\t\t\t\t\t\t\tdata.data[0].url = result.url.replace(/http:\\/\\//, 'https://')\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.then(() => onload(data))\n\t\t\t}\n\t\t\tfunctionAjax(url, param)\n\t\t}\n\t}\n})()"
  },
  {
    "path": "src/browser/manifest.json",
    "content": "{\n\t\"name\": \"UnblockNeteaseMusic\",\n\t\"description\": \"For test (es6 only)\",\n\t\"version\": \"0.1\",\n\t\"background\": {\n\t\t\"page\": \"background.html\"\n\t},\n\t\"content_scripts\": [{\n\t\t\"js\": [\"script.js\"],\n\t\t\"matches\": [\"*://music.163.com/*\"],\n\t\t\"all_frames\": true\n\t}],\n\t\"web_accessible_resources\": [\"inject.js\"],\n\t\"externally_connectable\": {\n\t\t\"matches\": [\"*://music.163.com/*\"]\n\t},\n\t\"manifest_version\": 2,\n\t\"permissions\": [\"*://*/*\", \"webRequest\", \"webRequestBlocking\"],\n\t\"content_security_policy\": \"script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net; object-src 'self'\"\n}"
  },
  {
    "path": "src/browser/request.js",
    "content": "export default (method, url, headers, body) => new Promise((resolve, reject) => {\n\theaders = headers || {}\n\tconst xhr = new XMLHttpRequest()\n\txhr.onreadystatechange = () => {if (xhr.readyState == 4) resolve(xhr)}\n\txhr.onerror = error => reject(error)\n\txhr.open(method, url, true)\n\tconst safe = {}, unsafe = {}\n\tObject.keys(headers).filter(key => (['origin', 'referer'].includes(key.toLowerCase()) ? unsafe : safe)[key] = headers[key])\n\tObject.entries(safe).forEach(entry => xhr.setRequestHeader.apply(xhr, entry))\n\tif (Object.keys(unsafe)) xhr.setRequestHeader('Additional-Headers', btoa(JSON.stringify(unsafe)))\n\txhr.send(body)\n}).then(xhr => Object.assign(xhr, {\n\tstatusCode: xhr.status,\n\theaders: \n\t\txhr.getAllResponseHeaders().split('\\r\\n').filter(line => line).map(line => line.split(/\\s*:\\s*/))\n\t\t.reduce((result, pair) => Object.assign(result, {[pair[0].toLowerCase()]: pair[1]}), {}),\n\turl: {href: xhr.responseURL},\n\tbody: () => xhr.responseText,\n\tjson: () => JSON.parse(xhr.responseText),\n\tjsonp: () => JSON.parse(xhr.responseText.slice(xhr.responseText.indexOf('(') + 1, -')'.length))\n}))"
  },
  {
    "path": "src/browser/script.js",
    "content": "(() => {\n\tlet script = (document.head || document.documentElement).appendChild(document.createElement('script'))\n\tscript.src = chrome.extension.getURL('inject.js')\n\tscript.onload = script.parentNode.removeChild(script)\n})()"
  },
  {
    "path": "src/cache.js",
    "content": "const collector = (job, cycle) =>\n\tsetTimeout(() => {\n\t\tlet keep = false\n\t\tObject.keys(job.cache || {})\n\t\t.forEach(key => {\n\t\t\tif (!job.cache[key]) return\n\t\t\tjob.cache[key].expiration < Date.now()\n\t\t\t\t? job.cache[key] = null\n\t\t\t\t: keep = keep || true\n\t\t})\n\t\tkeep ? collector(job, cycle) : job.collector = null\n\t}, cycle)\n\nmodule.exports = (job, parameter, live = 30 * 60 * 1000) => {\n\tconst cache = job.cache ? job.cache : job.cache = {}\n\tif (!job.collector) job.collector = collector(job, live / 2)\n\tconst key = parameter == null ? 'default' : (typeof(parameter) === 'object' ? (parameter.id || parameter.key || JSON.stringify(parameter)) : parameter)\n\tconst done = (status, result) => cache[key].execution = Promise[status](result)\n\tif (!cache[key] || cache[key].expiration < Date.now())\n\t\tcache[key] = {\n\t\t\texpiration: Date.now() + live,\n\t\t\texecution: job(parameter)\n\t\t\t\t.then(result => done('resolve', result))\n\t\t\t\t.catch(result => done('reject', result))\n\t\t}\n\treturn cache[key].execution\n}"
  },
  {
    "path": "src/cli.js",
    "content": "const cli = {\n\twidth: 80,\n\t_program: {},\n\t_options: [],\n\tprogram: (information = {}) => {\n\t\tcli._program = information\n\t\treturn cli\n\t},\n\toption: (flags, addition = {}) => {\n\t\t// name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.\n\t\t// dest - The name of the attribute to be added to the object returned by parse_options().\n\n\t\t// nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER\n\t\t// action - The basic type of action to be taken when this argument is encountered at the command line. // store, store_true, store_false, append, append_const, count, help, version\n\n\t\t// const - A constant value required by some action and nargs selections. (supporting store_const and append_const action)\n\n\t\t// metavar - A name for the argument in usage messages.\n\t\t// help - A brief description of what the argument does.\n\n\t\t// required - Whether or not the command-line option may be omitted (optionals only).\n\t\t// default - The value produced if the argument is absent from the command line.\n\t\t// type - The type to which the command-line argument should be converted.\n\t\t// choices - A container of the allowable values for the argument.\n\n\t\tflags = Array.isArray(flags) ? flags : [flags]\n\t\taddition.dest = addition.dest || flags.slice(-1)[0].toLowerCase().replace(/^-+/, '').replace(/-[a-z]/g, character => character.slice(1).toUpperCase())\n\t\taddition.help = addition.help || {'help': 'output usage information', 'version': 'output the version number'}[addition.action]\n\t\tcli._options.push(Object.assign(addition, {flags: flags, positional: !flags[0].startsWith('-')}))\n\t\treturn cli\n\t},\n\tparse: argv => {\n\t\tconst positionals = cli._options.map((option, index) => option.positional ? index : null).filter(index => index !== null), optionals = {}\n\t\tcli._options.forEach((option, index) => option.positional ? null : option.flags.forEach(flag => optionals[flag] = index))\n\n\t\tcli._program.name = cli._program.name || require('path').parse(argv[1]).base\n\t\tconst args = argv.slice(2).reduce((result, part) => /^-[^-]/.test(part) ? result.concat(part.slice(1).split('').map(string => '-' + string)) : result.concat(part), [])\n\n\t\tlet pointer = 0\n\t\twhile (pointer < args.length) {\n\t\t\tlet value = null\n\t\t\tconst part = args[pointer]\n\t\t\tconst index = part.startsWith('-') ? optionals[part] : positionals.shift()\n\t\t\tif (index == undefined) part.startsWith('-') ? error(`no such option: ${part}`) : error(`extra arguments found: ${part}`)\n\t\t\tif (part.startsWith('-')) pointer += 1\n\t\t\tconst {action} = cli._options[index]\n\n\t\t\tif (['help', 'version'].includes(action)) {\n\t\t\t\tif (action === 'help') help()\n\t\t\t\telse if (action === 'version') version()\n\t\t\t}\n\t\t\telse if (['store_true', 'store_false'].includes(action)) {\n\t\t\t\tvalue = action === 'store_true'\n\t\t\t}\n\t\t\telse {\n\t\t\t\tconst gap = args.slice(pointer).findIndex(part => part in optionals)\n\t\t\t\tconst next = gap === -1 ? args.length : pointer + gap\n\t\t\t\tvalue = args.slice(pointer, next)\n\t\t\t\tif (value.length === 0) {\n\t\t\t\t\tif (cli._options[index].positional)\n\t\t\t\t\t\terror(`the following arguments are required: ${part}`)\n\t\t\t\t\telse if (cli._options[index].nargs === '+')\n\t\t\t\t\t\terror(`argument ${part}: expected at least one argument`)\n\t\t\t\t\telse\n\t\t\t\t\t\terror(`argument ${part}: expected one argument`)\n\t\t\t\t}\n\t\t\t\tif (cli._options[index].nargs !== '+') {\n\t\t\t\t\tvalue = value[0]\n\t\t\t\t\tpointer += 1\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tpointer = next\n\t\t\t\t}\n\t\t\t}\n\t\t\tcli[cli._options[index].dest] = value\n\t\t}\n\t\tif (positionals.length) error(`the following arguments are required: ${positionals.map(index => cli._options[index].flags[0]).join(', ')}`)\n\t\t// cli._options.forEach(option => console.log(option.dest, cli[option.dest]))\n\t\treturn cli\n\t}\n}\n\nconst pad = length => (new Array(length + 1)).join(' ')\n\nconst usage = () => {\n\tconst options = cli._options.map(option => {\n\t\tconst flag = option.flags.sort((a, b) => a.length - b.length)[0]\n\t\tconst name = option.metavar || option.dest\n\t\tif (option.positional) {\n\t\t\tif (option.nargs === '+')\n\t\t\t\treturn `${name} [${name} ...]`\n\t\t\telse\n\t\t\t\treturn `${name}`\n\t\t}\n\t\telse {\n\t\t\tif (['store_true', 'store_false', 'help', 'version'].includes(option.action))\n\t\t\t\treturn `[${flag}]`\n\t\t\telse if (option.nargs === '+')\n\t\t\t\treturn `[${flag} ${name} [${name} ...]]`\n\t\t\telse\n\t\t\t\treturn `[${flag} ${name}]`\n\t\t}\n\t})\n\tconst maximum = cli.width\n\tconst title = `usage: ${cli._program.name}`\n\tconst lines = [title]\n\n\toptions.map(name => ' ' + name).forEach(option => {\n\t\tlines[lines.length - 1].length + option.length < maximum\n\t\t\t? lines[lines.length - 1] += option\n\t\t\t: lines.push(pad(title.length) + option)\n\t})\n\tconsole.log(lines.join('\\n'))\n}\n\nconst help = () => {\n\tusage()\n\tconst positionals = cli._options.filter(option => option.positional)\n\t.map(option => [option.metavar || option.dest, option.help])\n\tconst optionals = cli._options.filter(option => !option.positional)\n\t.map(option => {\n\t\tconst {flags} = option\n\t\tconst name = option.metavar || option.dest\n\t\tlet use = ''\n\t\tif (['store_true', 'store_false', 'help', 'version'].includes(option.action))\n\t\t\tuse = flags.map(flag => `${flag}`).join(', ')\n\t\telse if (option.nargs === '+')\n\t\t\tuse = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ')\n\t\telse\n\t\t\tuse = flags.map(flag => `${flag} ${name}`).join(', ')\n\t\treturn [use, option.help]\n\t})\n\tlet align = Math.max.apply(null, positionals.concat(optionals).map(option => option[0].length))\n\talign = align > 30 ? 30 : align\n\tconst rest = cli.width - align - 4\n\tconst publish = option => {\n\t\tconst slice = string =>\n\t\t\tArray.from(Array(Math.ceil(string.length / rest)).keys())\n\t\t\t.map(index => string.slice(index * rest, (index + 1) * rest))\n\t\t\t.join('\\n' + pad(align + 4))\n\t\toption[0].length < align\n\t\t\t? console.log(`  ${option[0]}${pad(align - option[0].length)}  ${slice(option[1])}`)\n\t\t\t: console.log(`  ${option[0]}\\n${pad(align + 4)}${slice(option[1])}`)\n\t}\n\tif (positionals.length) console.log('\\npositional arguments:')\n\tpositionals.forEach(publish)\n\tif (optionals.length) console.log('\\noptional arguments:')\n\toptionals.forEach(publish)\n\tprocess.exit()\n}\n\nconst version = () => {\n\tconsole.log(cli._program.version)\n\tprocess.exit()\n}\n\nconst error = message => {\n\tusage()\n\tconsole.log(cli._program.name + ':', 'error:', message)\n\tprocess.exit(1)\n}\n\nmodule.exports = cli"
  },
  {
    "path": "src/crypto.js",
    "content": "'use strict'\n\nconst crypto = require('crypto')\nconst parse = require('url').parse\nconst bodyify = require('querystring').stringify\n\nconst eapiKey = 'e82ckenh8dichen8'\nconst linuxapiKey = 'rFgB&h#%2?^eDg:Q'\n\nconst decrypt = (buffer, key) => {\n\tconst decipher = crypto.createDecipheriv('aes-128-ecb', key, '')\n\treturn Buffer.concat([decipher.update(buffer), decipher.final()])\n}\n\nconst encrypt = (buffer, key) => {\n\tconst cipher = crypto.createCipheriv('aes-128-ecb', key, '')\n\treturn Buffer.concat([cipher.update(buffer), cipher.final()])\n}\n\nmodule.exports = {\n\teapi: {\n\t\tencrypt: buffer => encrypt(buffer, eapiKey),\n\t\tdecrypt: buffer => decrypt(buffer, eapiKey),\n\t\tencryptRequest: (url, object) => {\n\t\t\turl = parse(url)\n\t\t\tconst text = JSON.stringify(object)\n\t\t\tconst message = `nobody${url.path}use${text}md5forencrypt`\n\t\t\tconst digest = crypto.createHash('md5').update(message).digest('hex')\n\t\t\tconst data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}`\n\t\t\treturn {\n\t\t\t\turl: url.href.replace(/\\w*api/, 'eapi'),\n\t\t\t\tbody: bodyify({\n\t\t\t\t\tparams: module.exports.eapi.encrypt(Buffer.from(data)).toString('hex').toUpperCase()\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t},\n\tlinuxapi: {\n\t\tencrypt: buffer => encrypt(buffer, linuxapiKey),\n\t\tdecrypt: buffer => decrypt(buffer, linuxapiKey),\n\t\tencryptRequest: (url, object) => {\n\t\t\turl = parse(url)\n\t\t\tconst text = JSON.stringify({method: 'POST', url: url.href, params: object})\n\t\t\treturn {\n\t\t\t\turl: url.resolve('/api/linux/forward'),\n\t\t\t\tbody: bodyify({\n\t\t\t\t\teparams: module.exports.linuxapi.encrypt(Buffer.from(text)).toString('hex').toUpperCase()\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t},\n\tmiguapi: {\n\t\tencryptBody: object => {\n\t\t\tconst text = JSON.stringify(object)\n\t\t\tconst derive = (password, salt, keyLength, ivSize) => { // EVP_BytesToKey\n\t\t\t\tsalt = salt || Buffer.alloc(0)\n\t\t\t\tconst keySize = keyLength / 8\n\t\t\t\tconst repeat = Math.ceil((keySize + ivSize * 8) / 32)\n\t\t\t\tconst buffer = Buffer.concat(Array(repeat).fill(null).reduce(\n\t\t\t\t\tresult => result.concat(crypto.createHash('md5').update(Buffer.concat([result.slice(-1)[0], password, salt])).digest()),\n\t\t\t\t\t[Buffer.alloc(0)]\n\t\t\t\t))\n\t\t\t\treturn {\n\t\t\t\t\tkey: buffer.slice(0, keySize),\n\t\t\t\t\tiv: buffer.slice(keySize, keySize + ivSize)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst password = Buffer.from(crypto.randomBytes(32).toString('hex')), salt = crypto.randomBytes(8)\n\t\t\tconst key = '-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\\n-----END PUBLIC KEY-----'\n\t\t\tconst secret = derive(password, salt, 256, 16)\n\t\t\tconst cipher = crypto.createCipheriv('aes-256-cbc', secret.key, secret.iv)\n\t\t\treturn bodyify({\n\t\t\t\tdata: Buffer.concat([Buffer.from('Salted__'), salt, cipher.update(Buffer.from(text)), cipher.final()]).toString('base64'),\n\t\t\t\tsecKey: crypto.publicEncrypt({key, padding: crypto.constants.RSA_PKCS1_PADDING}, password).toString('base64')\n\t\t\t})\n\t\t}\n\t},\n\tbase64: {\n\t\tencode: (text, charset) => Buffer.from(text, charset).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_'),\n\t\tdecode: (text, charset) => Buffer.from(text.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(charset)\n\t},\n\turi: {\n\t\tretrieve: id => {\n\t\t\tid = id.toString().trim()\n\t\t\tconst key = '3go8&$8*3*3h0k(2)2'\n\t\t\tconst string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('')\n\t\t\tconst result = crypto.createHash('md5').update(string).digest('base64').replace(/\\//g, '_').replace(/\\+/g, '-')\n\t\t\treturn `http://p1.music.126.net/${result}/${id}`\n\t\t}\n\t},\n\tmd5: {\n\t\tdigest: value => crypto.createHash('md5').update(value).digest('hex'),\n\t\tpipe: source => new Promise((resolve, reject) => {\n\t\t\tconst digest = crypto.createHash('md5').setEncoding('hex')\n\t\t\tsource.pipe(digest)\n\t\t\t.on('error', error => reject(error))\n\t\t\t.once('finish', () => resolve(digest.read()))\n\t\t})\n\t}\n}\n\ntry {module.exports.kuwoapi = require('./kwDES')} catch(e) {}"
  },
  {
    "path": "src/hook.js",
    "content": "const cache = require('./cache')\nconst parse = require('url').parse\nconst crypto = require('./crypto')\nconst request = require('./request')\nconst match = require('./provider/match')\nconst querystring = require('querystring')\n\nconst hook = {\n\trequest: {\n\t\tbefore: () => {},\n\t\tafter: () => {},\n\t},\n\tconnect: {\n\t\tbefore: () => {}\n\t},\n\tnegotiate: {\n\t\tbefore: () => {}\n\t},\n\ttarget: {\n\t\thost: new Set(),\n\t\tpath: new Set()\n\t}\n}\n\nhook.target.host = new Set([\n\t'music.163.com',\n\t'interface.music.163.com',\n\t'interface3.music.163.com',\n\t'apm.music.163.com',\n\t'apm3.music.163.com',\n\t// 'mam.netease.com',\n\t// 'api.iplay.163.com', // look living\n\t// 'ac.dun.163yun.com',\n\t// 'crash.163.com',\n\t// 'clientlog.music.163.com',\n\t// 'clientlog3.music.163.com'\n])\n\nhook.target.path = new Set([\n\t'/api/v3/playlist/detail',\n\t'/api/v3/song/detail',\n\t'/api/v6/playlist/detail',\n\t'/api/album/play',\n\t'/api/artist/privilege',\n\t'/api/album/privilege',\n\t'/api/v1/artist',\n\t'/api/v1/artist/songs',\n\t'/api/artist/top/song',\n\t'/api/v1/album',\n\t'/api/album/v3/detail',\n\t'/api/playlist/privilege',\n\t'/api/song/enhance/player/url',\n\t'/api/song/enhance/player/url/v1',\n\t'/api/song/enhance/download/url',\n\t'/api/song/enhance/privilege',\n\t'/batch',\n\t'/api/batch',\n\t'/api/v1/search/get',\n\t'/api/v1/search/song/get',\n\t'/api/search/complex/get',\n\t'/api/cloudsearch/pc',\n\t'/api/v1/playlist/manipulate/tracks',\n\t'/api/song/like',\n\t'/api/v1/play/record',\n\t'/api/playlist/v4/detail',\n\t'/api/v1/radio/get',\n\t'/api/v1/discovery/recommend/songs'\n])\n\nconst domainList = [\n\t'music.163.com', \n\t'music.126.net',\n\t'iplay.163.com',\n\t'look.163.com',\n\t'y.163.com',\n]\n\nhook.request.before = ctx => {\n\tconst {req} = ctx\n\treq.url = (req.url.startsWith('http://') ? '' : (req.socket.encrypted ? 'https:' : 'http:') + '//' + (domainList.some(domain => (req.headers.host || '').endsWith(domain)) ? req.headers.host : null)) + req.url\n\tconst url = parse(req.url)\n\tif ([url.hostname, req.headers.host].some(host => host.includes('music.163.com'))) ctx.decision = 'proxy'\n\tif ([url.hostname, req.headers.host].some(host => hook.target.host.has(host)) && req.method == 'POST' && (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/'))) {\n\t\treturn request.read(req)\n\t\t.then(body => req.body = body)\n\t\t.then(body => {\n\t\t\tif ('x-napm-retry' in req.headers) delete req.headers['x-napm-retry']\n\t\t\treq.headers['X-Real-IP'] = '118.88.88.88'\n\t\t\tif (req.url.includes('stream')) return // look living eapi can not be decrypted\n\t\t\tif (body) {\n\t\t\t\tlet data = null\n\t\t\t\tconst netease = {}\n\t\t\t\tnetease.pad = (body.match(/%0+$/) || [''])[0]\n\t\t\t\tnetease.forward = (url.path == '/api/linux/forward')\n\t\t\t\tif (netease.forward) {\n\t\t\t\t\tdata = JSON.parse(crypto.linuxapi.decrypt(Buffer.from(body.slice(8, body.length - netease.pad.length), 'hex')).toString())\n\t\t\t\t\tnetease.path = parse(data.url).path\n\t\t\t\t\tnetease.param = data.params\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tdata = crypto.eapi.decrypt(Buffer.from(body.slice(7, body.length - netease.pad.length), 'hex')).toString().split('-36cd479b6b5-')\n\t\t\t\t\tnetease.path = data[0]\n\t\t\t\t\tnetease.param = JSON.parse(data[1])\n\t\t\t\t}\n\t\t\t\tnetease.path = netease.path.replace(/\\/\\d*$/, '')\n\t\t\t\tctx.netease = netease\n\t\t\t\t// console.log(netease.path, netease.param)\n\n\t\t\t\tif (netease.path == '/api/song/enhance/download/url')\n\t\t\t\t\treturn pretendPlay(ctx)\n\t\t\t}\n\t\t})\n\t\t.catch(error => console.log(error, req.url))\n\t}\n\telse if ((hook.target.host.has(url.hostname)) && (url.path.startsWith('/weapi/') || url.path.startsWith('/api/'))) {\n\t\treq.headers['X-Real-IP'] = '118.88.88.88'\n\t\tctx.netease = {web: true, path: url.path.replace(/^\\/weapi\\//, '/api/').replace(/\\?.+$/, '').replace(/\\/\\d*$/, '')}\n\t}\n\telse if (req.url.includes('package')) {\n\t\ttry {\n\t\t\tconst data = req.url.split('package/').pop().split('/')\n\t\t\tconst url = parse(crypto.base64.decode(data[0]))\n\t\t\tconst id = data[1].replace(/\\.\\w+/, '')\n\t\t\treq.url = url.href\n\t\t\treq.headers['host'] = url.hostname\n\t\t\treq.headers['cookie'] = null\n\t\t\tctx.package = {id}\n\t\t\tctx.decision = 'proxy'\n\t\t\t// if (url.href.includes('google'))\n\t\t\t// \treturn request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080'))\n\t\t\t// \t.then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res)))\n\t\t}\n\t\tcatch(error) {\n\t\t\tctx.error = error\n\t\t\tctx.decision = 'close'\n\t\t}\n\t}\n}\n\nhook.request.after = ctx => {\n\tconst {req, proxyRes, netease, package} = ctx\n\tif (req.headers.host === 'tyst.migu.cn' && proxyRes.headers['content-range'] && proxyRes.statusCode === 200) proxyRes.statusCode = 206\n\tif (netease && hook.target.path.has(netease.path) && proxyRes.statusCode == 200) {\n\t\treturn request.read(proxyRes, true)\n\t\t.then(buffer => buffer.length ? proxyRes.body = buffer : Promise.reject())\n\t\t.then(buffer => {\n\t\t\tconst patch = string => string.replace(/([^\\\\]\"\\s*:\\s*)(\\d{16,})(\\s*[}|,])/g, '$1\"$2L\"$3') // for js precision\n\t\t\ttry {\n\t\t\t\tnetease.encrypted = false\n\t\t\t\tnetease.jsonBody = JSON.parse(patch(buffer.toString()))\n\t\t\t}\n\t\t\tcatch(error) {\n\t\t\t\tnetease.encrypted = true\n\t\t\t\tnetease.jsonBody = JSON.parse(patch(crypto.eapi.decrypt(buffer).toString()))\n\t\t\t}\n\n\t\t\tif (new Set([401, 512]).has(netease.jsonBody.code) && !netease.web) {\n\t\t\t\tif (netease.path.includes('manipulate')) return tryCollect(ctx)\n\t\t\t\telse if (netease.path == '/api/song/like') return tryLike(ctx)\n\t\t\t}\n\t\t\telse if (netease.path.includes('url')) return tryMatch(ctx)\n\t\t})\n\t\t.then(() => {\n\t\t\t['transfer-encoding', 'content-encoding', 'content-length'].filter(key => key in proxyRes.headers).forEach(key => delete proxyRes.headers[key])\n\n\t\t\tconst inject = (key, value) => {\n\t\t\t\tif (typeof(value) === 'object' && value != null) {\n\t\t\t\t\tif ('fee' in value) value['fee'] = 0\n\t\t\t\t\tif ('st' in value && 'pl' in value && 'dl' in value && 'subp' in value) { // batch modify\n\t\t\t\t\t\tvalue['st'] = 0\n\t\t\t\t\t\tvalue['subp'] = 1\n\t\t\t\t\t\tvalue['pl'] = (value['pl'] == 0) ? 320000 : value['pl']\n\t\t\t\t\t\tvalue['dl'] = (value['dl'] == 0) ? 320000 : value['dl']\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\tlet body = JSON.stringify(netease.jsonBody, inject)\n\t\t\tbody = body.replace(/([^\\\\]\"\\s*:\\s*)\"(\\d{16,})L\"(\\s*[}|,])/g, '$1$2$3') // for js precision\n\t\t\tproxyRes.body = (netease.encrypted ? crypto.eapi.encrypt(Buffer.from(body)) : body)\n\t\t})\n\t\t.catch(error => error ? console.log(error, req.url) : null)\n\t}\n\telse if (package) {\n\t\tif (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) {\n\t\t\treturn request(req.method, parse(req.url).resolve(proxyRes.headers.location), req.headers)\n\t\t\t.then(response => ctx.proxyRes = response)\n\t\t}\n\t\telse if (/p\\d+c*.music.126.net/.test(req.url)) {\n\t\t\tproxyRes.headers['content-type'] = 'audio/*'\n\t\t}\n\t}\n}\n\nhook.connect.before = ctx => {\n\tconst {req} = ctx\n\tconst url = parse('https://' + req.url)\n\tif ([url.hostname, req.headers.host].some(host => hook.target.host.has(host))) {\n\t\tif (url.port == 80) {\n\t\t\treq.url = `${global.address || 'localhost'}:${global.port[0]}`\n\t\t\treq.local = true\n\t\t}\n\t\telse if (global.port[1]) {\n\t\t\treq.url = `${global.address || 'localhost'}:${global.port[1]}`\n\t\t\treq.local = true\n\t\t}\n\t\telse {\n\t\t\tctx.decision = 'blank'\n\t\t}\n\t}\n\telse if (url.href.includes(global.endpoint)) ctx.decision = 'proxy'\n}\n\nhook.negotiate.before = ctx => {\n\tconst {req, socket, decision} = ctx\n\tconst url = parse('https://' + req.url)\n\tconst target = hook.target.host\n\tif (req.local || decision) return\n\tif (target.has(socket.sni) && !target.has(url.hostname)) {\n\t\ttarget.add(url.hostname)\n\t\tctx.decision = 'blank'\n\t}\n}\n\nconst pretendPlay = ctx => {\n\tconst {req, netease} = ctx\n\tconst turn = 'http://music.163.com/api/song/enhance/player/url'\n\tlet query = null\n\tif (netease.forward) {\n\t\tconst {id, br} = netease.param\n\t\tnetease.param = {ids: `[\"${id}\"]`, br}\n\t\tquery = crypto.linuxapi.encryptRequest(turn, netease.param)\n\t}\n\telse {\n\t\tconst {id, br, e_r, header} = netease.param\n\t\tnetease.param = {ids: `[\"${id}\"]`, br, e_r, header}\n\t\tquery = crypto.eapi.encryptRequest(turn, netease.param)\n\t}\n\treq.url = query.url\n\treq.body = query.body + netease.pad\n}\n\nconst tryCollect = ctx => {\n\tconst {req, netease} = ctx\n\tconst {trackIds, pid, op} = netease.param\n\tconst trackId = (Array.isArray(trackIds) ? trackIds : JSON.parse(trackIds))[0]\n\treturn request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=${op}`).then(response => response.json())\n\t.then(jsonBody => {\n\t\tnetease.jsonBody = jsonBody\n\t})\n\t.catch(() => {})\n}\n\nconst tryLike = ctx => {\n\tconst {req, netease} = ctx\n\tconst {trackId} = netease.param\n\tlet pid = 0, userId = 0\n\treturn request('GET', 'http://music.163.com/api/v1/user/info', req.headers).then(response => response.json())\n\t.then(jsonBody => {\n\t\tuserId = jsonBody.userPoint.userId\n\t\treturn request('GET', `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, req.headers).then(response => response.json())\n\t})\n\t.then(jsonBody => {\n\t\tpid = jsonBody.playlist[0].id\n\t\treturn request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add`).then(response => response.json())\n\t})\n\t.then(jsonBody => {\n\t\tif (new Set([200, 502]).has(jsonBody.code)) {\n\t\t\tnetease.jsonBody = {code: 200, playlistId: pid}\n\t\t}\n\t})\n\t.catch(() => {})\n}\n\nconst computeHash = task => request('GET', task.url).then(response => crypto.md5.pipe(response))\n\nconst tryMatch = ctx => {\n\tconst {req, netease} = ctx\n\tconst {jsonBody} = netease\n\tlet tasks = [], target = 0\n\n\tconst inject = item => {\n\t\titem.flag = 0\n\t\tif ((item.code != 200 || item.freeTrialInfo) && (target == 0 || item.id == target)) {\n\t\t\treturn match(item.id)\n\t\t\t.then(song => {\n\t\t\t\titem.type = song.br === 999000 ? 'flac' : 'mp3'\n\t\t\t\titem.url = global.endpoint ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${item.id}.${item.type}` : song.url\n\t\t\t\titem.md5 = song.md5 || crypto.md5.digest(song.url)\n\t\t\t\titem.br = song.br || 128000\n\t\t\t\titem.size = song.size\n\t\t\t\titem.code = 200\n\t\t\t\titem.freeTrialInfo = null\n\t\t\t\treturn song\n\t\t\t})\n\t\t\t.then(song => {\n\t\t\t\tif (!netease.path.includes('download') || song.md5) return\n\t\t\t\tconst newer = (base, target) => {\n\t\t\t\t\tconst difference =\n\t\t\t\t\t\tArray.from([base, target])\n\t\t\t\t\t\t.map(version => version.split('.').slice(0, 3).map(number => parseInt(number) || 0))\n\t\t\t\t\t\t.reduce((aggregation, current) => !aggregation.length ? current.map(element => [element]) : aggregation.map((element, index) => element.concat(current[index])), [])\n\t\t\t\t\t\t.filter(pair => pair[0] != pair[1])[0]\n\t\t\t\t\treturn !difference || difference[0] <= difference[1]\n\t\t\t\t}\n\t\t\t\tconst limit = {android: '0.0.0', osx: '0.0.0'}\n\t\t\t\tconst task = {key: song.url.replace(/\\?.*$/, '').replace(/(?<=kugou\\.com\\/)\\w+\\/\\w+\\//, '').replace(/(?<=kuwo\\.cn\\/)\\w+\\/\\w+\\/resource\\//, ''), url: song.url}\n\t\t\t\ttry {\n\t\t\t\t\tlet {header} = netease.param\n\t\t\t\t\theader = typeof(header) === 'string' ? JSON.parse(header) : header\n\t\t\t\t\tconst cookie = querystring.parse(req.headers.cookie.replace(/\\s/g, ''), ';')\n\t\t\t\t\tconst os = header.os || cookie.os, version = header.appver || cookie.appver\n\t\t\t\t\tif (os in limit && newer(limit[os], version))\n\t\t\t\t\t\treturn cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value)\n\t\t\t\t}\n\t\t\t\tcatch(e) {}\n\t\t\t})\n\t\t\t.catch(() => {})\n\t\t}\n\t\telse if (item.code == 200 && netease.web) {\n\t\t\titem.url = item.url.replace(/(m\\d+?)(?!c)\\.music\\.126\\.net/, '$1c.music.126.net')\n\t\t}\n\t}\n\n\tif (!Array.isArray(jsonBody.data)) {\n\t\ttasks = [inject(jsonBody.data)]\n\t}\n\telse if (netease.path.includes('download')) {\n\t\tjsonBody.data = jsonBody.data[0]\n\t\ttasks = [inject(jsonBody.data)]\n\t}\n\telse {\n\t\ttarget = netease.web ? 0 : parseInt(((Array.isArray(netease.param.ids) ? netease.param.ids : JSON.parse(netease.param.ids))[0] || 0).toString().replace('_0', '')) // reduce time cost\n\t\ttasks = jsonBody.data.map(item => inject(item))\n\t}\n\treturn Promise.all(tasks).catch(() => {})\n}\n\nmodule.exports = hook"
  },
  {
    "path": "src/kwDES.js",
    "content": "/*\n\tThanks to\n\thttps://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py\n\thttps://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java\n*/\n\nconst Long = (\n\ttypeof(BigInt) === 'function' // BigInt support in Node 10+\n\t\t? n => (n = BigInt(n), ({\n\t\t\tlow: Number(n),\n\t\t\tvalueOf: () => n.valueOf(),\n\t\t\ttoString: () => n.toString(),\n\t\t\tnot: () => Long(~n),\n\t\t\tisNegative: () => n < 0,\n\t\t\tor: x => Long(n | BigInt(x)),\n\t\t\tand: x => Long(n & BigInt(x)),\n\t\t\txor: x => Long(n ^ BigInt(x)),\n\t\t\tequals: x => n === BigInt(x),\n\t\t\tmultiply: x => Long(n * BigInt(x)),\n\t\t\tshiftLeft: x => Long(n << BigInt(x)),\n\t\t\tshiftRight: x => Long(n >> BigInt(x)),\n\t\t}))\n\t\t: (...args) => new (require('long'))(...args)\n)\n\nconst range = n => Array.from(new Array(n).keys())\nconst power = (base, index) => Array(index).fill().reduce((result) => result.multiply(base), Long(1))\nconst LongArray = (...array) => array.map(n => n === -1 ? Long(-1, -1) : Long(n))\n\n// EXPANSION\nconst arrayE = LongArray(\n\t31,  0,  1,  2,  3,  4, -1, -1,\n\t 3,  4,  5,  6,  7,  8, -1, -1,\n\t 7,  8,  9, 10, 11, 12, -1, -1,\n\t11, 12, 13, 14, 15, 16, -1, -1,\n\t15, 16, 17, 18, 19, 20, -1, -1,\n\t19, 20, 21, 22, 23, 24, -1, -1,\n\t23, 24, 25, 26, 27, 28, -1, -1,\n\t27, 28, 29, 30, 31, 30, -1, -1\n)\n\n// INITIAL_PERMUTATION\nconst arrayIP = LongArray(\n\t57, 49, 41, 33, 25, 17,  9,  1,\n\t59, 51, 43, 35, 27, 19, 11,  3,\n\t61, 53, 45, 37, 29, 21, 13,  5,\n\t63, 55, 47, 39, 31, 23, 15,  7,\n\t56, 48, 40, 32, 24, 16,  8,  0,\n\t58, 50, 42, 34, 26, 18, 10,  2,\n\t60, 52, 44, 36, 28, 20, 12,  4,\n\t62, 54, 46, 38, 30, 22, 14,  6\n)\n\n// INVERSE_PERMUTATION\nconst arrayIP_1 = LongArray(\n\t39,  7, 47, 15, 55, 23, 63, 31,\n\t38,  6, 46, 14, 54, 22, 62, 30,\n\t37,  5, 45, 13, 53, 21, 61, 29,\n\t36,  4, 44, 12, 52, 20, 60, 28,\n\t35,  3, 43, 11, 51, 19, 59, 27,\n\t34,  2, 42, 10, 50, 18, 58, 26,\n\t33,  1, 41,  9, 49, 17, 57, 25,\n\t32,  0, 40,  8, 48, 16, 56, 24\n)\n\n// ROTATES\nconst arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]\nconst arrayLsMask = LongArray(0, 0x100001, 0x300003)\nconst arrayMask = range(64).map(n => power(2, n))\narrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1)\n\n// PERMUTATION\nconst arrayP = LongArray(\n\t15,  6, 19, 20, 28, 11, 27, 16,\n\t 0, 14, 22, 25,  4, 17, 30,  9,\n\t 1,  7, 23, 13, 31, 26,  2,  8,\n\t18, 12, 29,  5, 21, 10,  3, 24\n)\n\n// PERMUTED_CHOICE1\nconst arrayPC_1 = LongArray(\n\t56, 48, 40, 32, 24, 16,  8,  0,\n\t57, 49, 41, 33, 25, 17,  9,  1,\n\t58, 50, 42, 34, 26, 18, 10,  2,\n\t59, 51, 43, 35, 62, 54, 46, 38,\n\t30, 22, 14,  6, 61, 53, 45, 37,\n\t29, 21, 13,  5, 60, 52, 44, 36,\n\t28, 20, 12,  4, 27, 19, 11,  3\n)\n\n// PERMUTED_CHOICE2\nconst arrayPC_2 = LongArray(\n\t13, 16, 10, 23,  0,  4, -1, -1,\n\t 2, 27, 14,  5, 20,  9, -1, -1,\n\t22, 18, 11,  3, 25,  7, -1, -1,\n\t15,  6, 26, 19, 12,  1, -1, -1,\n\t40, 51, 30, 36, 46, 54, -1, -1,\n\t29, 39, 50, 44, 32, 47, -1, -1,\n\t43, 48, 38, 55, 33, 52, -1, -1,\n\t45, 41, 49, 35, 28, 31, -1, -1\n)\n\nconst matrixNSBox = [[\n\t14,  4,  3, 15,  2, 13,  5,  3,\n\t13, 14,  6,  9, 11,  2,  0,  5,\n\t 4,  1, 10, 12, 15,  6,  9, 10,\n\t 1,  8, 12,  7,  8, 11,  7,  0,\n\t 0, 15, 10,  5, 14,  4,  9, 10,\n\t 7,  8, 12,  3, 13,  1,  3,  6,\n\t15, 12,  6, 11,  2,  9,  5,  0,\n\t 4,  2, 11, 14,  1,  7,  8, 13, ], [\n\t15,  0,  9,  5,  6, 10, 12,  9,\n\t 8,  7,  2, 12,  3, 13,  5,  2,\n\t 1, 14,  7,  8, 11,  4,  0,  3,\n\t14, 11, 13,  6,  4,  1, 10, 15,\n\t 3, 13, 12, 11, 15,  3,  6,  0,\n\t 4, 10,  1,  7,  8,  4, 11, 14,\n\t13,  8,  0,  6,  2, 15,  9,  5,\n\t 7,  1, 10, 12, 14,  2,  5,  9, ], [\n\t10, 13,  1, 11,  6,  8, 11,  5,\n\t 9,  4, 12,  2, 15,  3,  2, 14,\n\t 0,  6, 13,  1,  3, 15,  4, 10,\n\t14,  9,  7, 12,  5,  0,  8,  7,\n\t13,  1,  2,  4,  3,  6, 12, 11,\n\t 0, 13,  5, 14,  6,  8, 15,  2,\n\t 7, 10,  8, 15,  4,  9, 11,  5,\n\t 9,  0, 14,  3, 10,  7,  1, 12, ], [\n\t 7, 10,  1, 15,  0, 12, 11,  5,\n\t14,  9,  8,  3,  9,  7,  4,  8,\n\t13,  6,  2,  1,  6, 11, 12,  2,\n\t 3,  0,  5, 14, 10, 13, 15,  4,\n\t13,  3,  4,  9,  6, 10,  1, 12,\n\t11,  0,  2,  5,  0, 13, 14,  2,\n\t 8, 15,  7,  4, 15,  1, 10,  7,\n\t 5,  6, 12, 11,  3,  8,  9, 14, ], [\n\t 2,  4,  8, 15,  7, 10, 13,  6,\n\t 4,  1,  3, 12, 11,  7, 14,  0,\n\t12,  2,  5,  9, 10, 13,  0,  3,\n\t 1, 11, 15,  5,  6,  8,  9, 14,\n\t14, 11,  5,  6,  4,  1,  3, 10,\n\t 2, 12, 15,  0, 13,  2,  8,  5,\n\t11,  8,  0, 15,  7, 14,  9,  4,\n\t12,  7, 10,  9,  1, 13,  6,  3, ], [\n\t12,  9,  0,  7,  9,  2, 14,  1,\n\t10, 15,  3,  4,  6, 12,  5, 11,\n\t 1, 14, 13,  0,  2,  8,  7, 13,\n\t15,  5,  4, 10,  8,  3, 11,  6,\n\t10,  4,  6, 11,  7,  9,  0,  6,\n\t 4,  2, 13,  1,  9, 15,  3,  8,\n\t15,  3,  1, 14, 12,  5, 11,  0,\n\t 2, 12, 14,  7,  5, 10,  8, 13, ], [\n\t 4,  1,  3, 10, 15, 12,  5,  0,\n\t 2, 11,  9,  6,  8,  7,  6,  9,\n\t11,  4, 12, 15,  0,  3, 10,  5,\n\t14, 13,  7,  8, 13, 14,  1,  2,\n\t13,  6, 14,  9,  4,  1,  2, 14,\n\t11, 13,  5,  0,  1, 10,  8,  3,\n\t 0, 11,  3,  5,  9,  4, 15,  2,\n\t 7,  8, 12, 15, 10,  7,  6, 12, ], [\n\t13,  7, 10,  0,  6,  9,  5, 15,\n\t 8,  4,  3, 10, 11, 14, 12,  5,\n\t 2, 11,  9,  6, 15, 12,  0,  3,\n\t 4,  1, 14, 13,  1,  2,  7,  8,\n\t 1,  2, 12, 15, 10,  4,  0,  3,\n\t13, 14,  6,  9,  7,  8,  9,  6,\n\t15,  1,  5, 12,  3, 10, 14,  5,\n\t 8,  7, 11,  0,  4, 13,  2, 11, ],\n]\n\nconst bitTransform = (arrInt, n, l) => { // int[], int, long : long\n\tlet l2 = Long(0)\n\trange(n).forEach(i => {\n\t\tif (arrInt[i].isNegative() || (l.and(arrayMask[arrInt[i].low]).equals(0)))\n\t\t\treturn\n\t\tl2 = l2.or(arrayMask[i])\n\t})\n\treturn l2\n}\n\nconst DES64 = (longs, l) => { // long[], long\n\tlet out = Long(0)\n\tlet SOut = Long(0)\n\tconst pR = range(8).map(() => Long(0))\n\tconst pSource = [Long(0), Long(0)]\n\tlet L = Long(0)\n\tlet R = Long(0)\n\tout = bitTransform(arrayIP, 64, l)\n\tpSource[0] = out.and(0xFFFFFFFF)\n\tpSource[1] = out.and(-4294967296).shiftRight(32)\n\n\trange(16).forEach(i => {\n\t\tR = Long(pSource[1])\n\t\tR = bitTransform(arrayE, 64, R)\n\t\tR = R.xor(longs[i])\n\t\trange(8).forEach(j => {\n\t\t\tpR[j] = R.shiftRight(j * 8).and(255)\n\t\t})\n\t\tSOut = Long(0)\n\t\trange(8).reverse().forEach(sbi => {\n\t\t\tSOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]])\n\t\t})\n\t\tR = bitTransform(arrayP, 32, SOut)\n\t\tL = Long(pSource[0])\n\t\tpSource[0] = Long(pSource[1])\n\t\tpSource[1] = L.xor(R)\n\t})\n\tpSource.reverse()\n\tout = pSource[1].shiftLeft(32).and(-4294967296).or(\n\t\tpSource[0].and(0xFFFFFFFF)\n\t)\n\tout = bitTransform(arrayIP_1, 64, out)\n\treturn out\n}\n\n\nconst subKeys = (l, longs, n) => { // long, long[], int\n\tlet l2 = bitTransform(arrayPC_1, 56, l)\n\trange(16).forEach(i => {\n\t\tl2 = (\n\t\t\tl2.and(arrayLsMask[arrayLs[i]]).shiftLeft(28 - arrayLs[i]).or(\n\t\t\t\tl2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i])\n\t\t\t)\n\t\t)\n\t\tlongs[i] = bitTransform(arrayPC_2, 64, l2)\n\t})\n\tif (n === 1) {\n\t\trange(8).forEach(j => {\n\t\t\t[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]]\n\t\t})\n\t}\n}\n\nconst crypt = (msg, key, mode) => {\n\t// 处理密钥块\n\tlet l = Long(0)\n\trange(8).forEach(i => {\n\t\tl = Long(key[i]).shiftLeft(i * 8).or(l)\n\t})\n\n\tconst j = Math.floor(msg.length / 8)\n\t// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了\n\t\n\tconst arrLong1 = range(16).map(() => Long(0))\n\tsubKeys(l, arrLong1, mode)\n\n\t// arrLong2 存放的是前部分的明文\n\tconst arrLong2 = range(j).map(() => Long(0))\n\n\trange(j).forEach(m => {\n\t\trange(8).forEach(n => {\n\t\t\tarrLong2[m] = Long(msg[n + m * 8]).shiftLeft(n * 8).or(arrLong2[m])\n\t\t})\n\t})\n\n\t// 用于存放密文\n\tconst arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0))\n\n\t// 计算前部的数据块(除了最后一部分)\n\trange(j).forEach(i1 => {\n\t\tarrLong3[i1] = DES64(arrLong1, arrLong2[i1])\n\t})\n\n\t// 保存多出来的字节\n\tconst arrByte1 = msg.slice(j * 8)\n\tlet l2 = Long(0)\n\n\trange(msg.length % 8).forEach(i1 => {\n\t\tl2 = Long(arrByte1[i1]).shiftLeft(i1 * 8).or(l2)\n\t})\n\n\t// 计算多出的那一位(最后一位)\n\tif (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2) // 解密不需要\n\n\t// 将密文转为字节型\n\tconst arrByte2 = range(8 * arrLong3.length).map(() => 0)\n\tlet i4 = 0\n\tarrLong3.forEach(l3 => {\n\t\trange(8).forEach(i6 => {\n\t\t\tarrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low\n\t\t\ti4 += 1\n\t\t})\n\t})\n\treturn Buffer.from(arrByte2)\n}\n\nconst SECRET_KEY = Buffer.from('ylzsxkwm')\nconst encrypt = msg => crypt(msg, SECRET_KEY, 0)\nconst decrypt = msg => crypt(msg, SECRET_KEY, 1)\nconst encryptQuery = query => encrypt(Buffer.from(query)).toString('base64')\n\nmodule.exports = {encrypt, decrypt, encryptQuery}"
  },
  {
    "path": "src/provider/baidu.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst request = require('../request')\n\nconst format = song => {\n\tconst artistId = song.all_artist_id.split(',')\n\treturn {\n\t\tid: song.song_id,\n\t\tname: song.title,\n\t\talbum: {id: song.album_id, name: song.album_title},\n\t\tartists: song.author.split(',').map((name, index) => ({id: artistId[index], name}))\n\t}\n}\n\nconst search = info => {\n\tconst url =\n\t\t'http://musicapi.taihe.com/v1/restserver/ting?' +\n\t\t'from=qianqianmini&method=baidu.ting.search.merge&' +\n\t\t'isNew=1&platform=darwin&page_no=1&page_size=30&' +\n\t\t`query=${encodeURIComponent(info.keyword)}&version=11.2.1`\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst list = jsonBody.result.song_info.song_list.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tconst url =\n\t\t'http://music.taihe.com/data/music/fmlink?' +\n\t\t'songIds=' + id + '&type=mp3'\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tif ('songList' in jsonBody.data)\n\t\t\treturn jsonBody.data.songList[0].songLink || Promise.reject()\n\t\telse\n\t\t\treturn Promise.reject()\n\t})\n\t.catch(() => insure().baidu.track(id))\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check}"
  },
  {
    "path": "src/provider/find.js",
    "content": "const cache = require('../cache')\nconst request = require('../request')\n\nconst filter = (object, keys) => Object.keys(object).reduce((result, key) => Object.assign(result, keys.includes(key) && {[key]: object[key]}), {})\n// Object.keys(object).filter(key => !keys.includes(key)).forEach(key => delete object[key])\n\nconst limit = text => {\n\tconst output = [text[0]]\n\tconst length = () => output.reduce((sum, token) => sum + token.length, 0)\n\ttext.slice(1).some(token => length() > 15 ? true : (output.push(token), false))\n\treturn output\n}\n\nconst find = id => {\n\tconst url =\n\t\t'https://music.163.com/api/song/detail?ids=[' + id + ']'\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst info = filter(jsonBody.songs[0], ['id', 'name', 'alias', 'duration'])\n\t\tinfo.name = (info.name || '')\n\t\t\t.replace(/（\\s*cover[:：\\s][^）]+）/i, '')\n\t\t\t.replace(/\\(\\s*cover[:：\\s][^\\)]+\\)/i, '')\n\t\t\t.replace(/（\\s*翻自[:：\\s][^）]+）/, '')\n\t\t\t.replace(/\\(\\s*翻自[:：\\s][^\\)]+\\)/, '')\n\t\tinfo.album = filter(jsonBody.songs[0].album, ['id', 'name'])\n\t\tinfo.artists = jsonBody.songs[0].artists.map(artist => filter(artist, ['id', 'name']))\n\t\tinfo.keyword = info.name + ' - ' + limit(info.artists.map(artist => artist.name)).join(' / ')\n\t\treturn info.name ? info : Promise.reject()\n\t})\n}\n\nmodule.exports = id => cache(find, id)"
  },
  {
    "path": "src/provider/insure.js",
    "content": "const request = require('../request')\nconst host = null // 'http://localhost:9000'\n\nmodule.exports = () => {\n\tconst proxy = new Proxy(() => {}, {\n\t\tget: (target, property) => {\n\t\t\ttarget.route = (target.route || []).concat(property)\n\t\t\treturn proxy\n\t\t},\n\t\tapply: (target, _, payload) => {\n\t\t\tif (module.exports.disable || !host) return Promise.reject()\n\t\t\tconst path = target.route.join('/')\n\t\t\tconst query = typeof(payload[0]) === 'object' ? JSON.stringify(payload[0]) : payload[0]\n\t\t\t// if (path != 'qq/ticket') return Promise.reject()\n\t\t\treturn request('GET', `${host}/${path}?${encodeURIComponent(query)}`)\n\t\t\t.then(response => response.body())\n\t\t}\n\t})\n\treturn proxy\n}"
  },
  {
    "path": "src/provider/joox.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst headers = {\n\t'origin': 'http://www.joox.com',\n\t'referer': 'http://www.joox.com'\n}\n\nconst fit = info => {\n\tif (/[\\u0800-\\u4e00]/.test(info.name)) //is japanese\n\t\treturn info.name\n\telse\n\t\treturn info.keyword\n}\n\nconst format = song => {\n\tconst {decode} = crypto.base64\n\treturn {\n\t\tid: song.songid,\n\t\tname: decode(song.info1 || ''),\n\t\tduration: song.playtime * 1000,\n\t\talbum: {id: song.albummid, name: decode(song.info3 || '')},\n\t\tartists: song.singer_list.map(({id, name}) => ({id, name: decode(name || '')}))\n\t}\n}\n\nconst search = info => {\n\tconst keyword = fit(info)\n\tconst url =\n\t\t'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' +\n\t\t'country=hk&lang=zh_TW&' +\n\t\t'search_input=' + encodeURIComponent(keyword) + '&sin=0&ein=30'\n\n\treturn request('GET', url, headers)\n\t.then(response => response.body())\n\t.then(body => {\n\t\tconst jsonBody = JSON.parse(body.replace(/'/g, '\"'))\n\t\tconst list = jsonBody.itemlist.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tconst url =\n\t\t'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' +\n\t\t'songid=' + id + '&country=hk&lang=zh_cn&from_type=-1&' +\n\t\t'channel_id=-1&_=' + (new Date).getTime()\n\n\treturn request('GET', url, headers)\n\t.then(response => response.jsonp())\n\t.then(jsonBody => {\n\t\tconst songUrl = (jsonBody.r320Url || jsonBody.r192Url || jsonBody.mp3Url || jsonBody.m4aUrl).replace(/M\\d00([\\w]+).mp3/, 'M800$1.mp3')\n\t\tif (songUrl)\n\t\t\treturn songUrl\n\t\telse\n\t\t\treturn Promise.reject()\n\t})\n\t.catch(() => insure().joox.track(id))\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, track}"
  },
  {
    "path": "src/provider/kugou.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst format = song => {\n\tconst SingerName = song.SingerName.split('、')\n\treturn {\n\t\tid: song.FileHash,\n\t\tname: song.SongName,\n\t\tduration: song.Duration * 1000,\n\t\talbum: {id: song.AlbumID, name: song.AlbumName},\n\t\tartists: song.SingerId.map((id, index) => ({id, name: SingerName[index]}))\n\t}\n}\n\nconst search = info => {\n\tconst url =\n\t\t'http://songsearch.kugou.com/song_search_v2?' +\n\t\t'keyword=' + encodeURIComponent(info.keyword) + '&page=1'\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst list = jsonBody.data.lists.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n\t.catch(() => insure().kugou.search(info))\n}\n\nconst track = id => {\n\t// const url =\n\t// \t'http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=' + id\n\n\t// return request('GET', url)\n\t// .then(response => response.json())\n\t// .then(jsonBody => jsonBody.url || Promise.reject())\n\n\tconst url =\n\t\t'http://trackercdn.kugou.com/i/v2/?' +\n\t\t'key=' + crypto.md5.digest(`${id}kgcloudv2`) + '&hash=' + id + '&' +\n\t\t'br=hq&appid=1005&pid=2&cmd=25&behavior=play'\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => jsonBody.url[0] || Promise.reject())\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, search}"
  },
  {
    "path": "src/provider/kuwo.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst format = song => ({\n\tid: song.musicrid.split('_').pop(),\n\tname: song.name,\n\tduration: song.songTimeMinutes.split(':').reduce((minute, second) => minute * 60 + parseFloat(second), 0) * 1000,\n\talbum: {id: song.albumid, name: song.album},\n\tartists: song.artist.split('&').map((name, index) => ({id: index ? null : song.artistid, name}))\n})\n\nconst search = info => {\n\t// const url =\n\t// \t// 'http://search.kuwo.cn/r.s?' +\n\t// \t// 'ft=music&itemset=web_2013&client=kt&' +\n\t// \t// 'rformat=json&encoding=utf8&' +\n\t// \t// 'all=' + encodeURIComponent(info.keyword) + '&pn=0&rn=20'\n\t// \t'http://search.kuwo.cn/r.s?' +\n\t// \t'ft=music&rformat=json&encoding=utf8&' +\n\t// \t'rn=8&callback=song&vipver=MUSIC_8.0.3.1&' +\n\t// \t'SONGNAME=' + encodeURIComponent(info.name) + '&' +\n\t// \t'ARTIST=' + encodeURIComponent(info.artists[0].name)\n\n\t// return request('GET', url)\n\t// .then(response => response.body())\n\t// .then(body => {\n\t// \tconst jsonBody = eval(\n\t// \t\t'(' + body\n\t// \t\t.replace(/\\n/g, '')\n\t// \t\t.match(/try\\s*\\{[^=]+=\\s*(.+?)\\s*\\}\\s*catch/)[1]\n\t// \t\t.replace(/;\\s*song\\s*\\(.+\\)\\s*;\\s*/, '') + ')'\n\t// \t)\n\t// \tconst matched = jsonBody.abslist[0]\n\t// \tif (matched)\n\t// \t\treturn matched.MUSICRID.split('_').pop()\n\t// \telse\n\t// \t\treturn Promise.reject()\n\t// })\n\n\tconst keyword = encodeURIComponent(info.keyword.replace(' - ', ''))\n\tconst url = `http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=${keyword}&pn=1&rn=30`\n\n\treturn request('GET', `http://kuwo.cn/search/list?key=${keyword}`)\n\t.then(response => response.headers['set-cookie'].find(line => line.includes('kw_token')).replace(/;.*/, '').split('=').pop())\n\t.then(token => request('GET', url, {referer: `http://www.kuwo.cn/search/list?key=${keyword}`, csrf: token, cookie: `kw_token=${token}`}))\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst list = jsonBody.data.list.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tconst url = (crypto.kuwoapi\n\t\t? 'http://mobi.kuwo.cn/mobi.s?f=kuwo&q=' + crypto.kuwoapi.encryptQuery(\n\t\t\t'corp=kuwo&p2p=1&type=convert_url2&sig=0&format=' + ['flac', 'mp3'].slice(select.ENABLE_FLAC ? 0 : 1).join('|') + '&rid=' + id\n\t\t)\n\t\t: 'http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_' + id // flac refuse\n\t\t// : 'http://www.kuwo.cn/url?format=mp3&response=url&type=convert_url3&br=320kmp3&rid=' + id // flac refuse\n\t)\n\n\treturn request('GET', url, {'user-agent': 'okhttp/3.10.0'})\n\t.then(response => response.body())\n\t.then(body => {\n\t\tconst url = (body.match(/http[^\\s$\"]+/) || [])[0]\n\t\treturn url || Promise.reject()\n\t})\n\t.catch(() => insure().kuwo.track(id))\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, track}\n"
  },
  {
    "path": "src/provider/match.js",
    "content": "const find = require('./find')\nconst request = require('../request')\n\nconst provider = {\n\tnetease: require('./netease'),\n\tqq: require('./qq'),\n\txiami: require('./xiami'),\n\tbaidu: require('./baidu'),\n\tkugou: require('./kugou'),\n\tkuwo: require('./kuwo'),\n\tmigu: require('./migu'),\n\tjoox: require('./joox'),\n\tyoutube: require('./youtube')\n}\n\nconst match = (id, source) => {\n\tlet meta = {}\n\tconst candidate = (source || global.source || ['qq', 'kuwo', 'migu']).filter(name => name in provider)\n\treturn find(id)\n\t.then(info => {\n\t\tmeta = info\n\t\treturn Promise.all(candidate.map(name => provider[name].check(info).catch(() => {})))\n\t})\n\t.then(urls => {\n\t\turls = urls.filter(url => url)\n\t\treturn Promise.all(urls.map(url => check(url)))\n\t})\n\t.then(songs => {\n\t\tsongs = songs.filter(song => song.url)\n\t\tif (!songs.length) return Promise.reject()\n\t\tconsole.log(`[${meta.id}] ${meta.name}\\n${songs[0].url}`)\n\t\treturn songs[0]\n\t})\n}\n\nconst check = url => {\n\tconst song = {size: 0, br: null, url: null, md5: null}\n\treturn Promise.race([request('GET', url, {'range': 'bytes=0-8191'}), new Promise((_, reject) => setTimeout(() => reject(504), 5 * 1000))])\n\t.then(response => {\n\t\tif (!response.statusCode.toString().startsWith('2')) return Promise.reject()\n\t\tif (url.includes('qq.com'))\n\t\t\tsong.md5 = response.headers['server-md5']\n\t\telse if (url.includes('xiami.net') || url.includes('qianqian.com'))\n\t\t\tsong.md5 = response.headers['etag'].replace(/\"/g, '').toLowerCase()\n\t\tsong.size = parseInt((response.headers['content-range'] || '').split('/').pop() || response.headers['content-length']) || 0\n\t\tsong.url = response.url.href\n\t\treturn response.headers['content-length'] === '8192' ? response.body(true) : Promise.reject()\n\t})\n\t.then(data => {\n\t\tconst bitrate = decode(data)\n\t\tsong.br = (bitrate && !isNaN(bitrate)) ? bitrate * 1000 : null\n\t})\n\t.catch(() => {})\n\t.then(() => song)\n}\n\nconst decode = buffer => {\n\tconst map = {\n\t\t3: {\n\t\t\t3: ['free', 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 'bad'],\n\t\t\t2: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 'bad'],\n\t\t\t1: ['free', 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 'bad']\n\t\t},\n\t\t2: {\n\t\t\t3: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 'bad'],\n\t\t\t2: ['free', 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 'bad']\n\t\t}\n\t}\n\tmap[2][1] = map[2][2]\n\tmap[0] = map[2]\n\n\tlet pointer = 0\n\tif (buffer.slice(0, 4).toString() === 'fLaC') return 999\n\tif (buffer.slice(0, 3).toString() === 'ID3') {\n\t\tpointer = 6\n\t\tconst size = buffer.slice(pointer, pointer + 4).reduce((summation, value, index) => summation + (value & 0x7f) << (7 * (3 - index)), 0)\n\t\tpointer = 10 + size\n\t}\n\tconst header = buffer.slice(pointer, pointer + 4)\n\n\t// https://www.allegro.cc/forums/thread/591512/674023\n\tif (\n\t\theader.length === 4 &&\n\t\theader[0] === 0xff &&\n\t\t((header[1] >> 5) & 0x7) === 0x7 &&\n\t\t((header[1] >> 1) & 0x3) !== 0 &&\n\t\t((header[2] >> 4) & 0xf) !== 0xf &&\n\t\t((header[2] >> 2) & 0x3) !== 0x3\n\t) {\n\t\tconst version = (header[1] >> 3) & 0x3\n\t\tconst layer = (header[1] >> 1) & 0x3\n\t\tconst bitrate = header[2] >> 4\n\t\treturn map[version][layer][bitrate]\n\t}\n}\n\nmodule.exports = match"
  },
  {
    "path": "src/provider/migu.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst headers = {\n\t'origin': 'http://music.migu.cn/',\n\t'referer': 'http://music.migu.cn/'\n}\n\nconst format = song => {\n\tconst singerId = song.singerId.split(/\\s*,\\s*/)\n\tconst singerName = song.singerName.split(/\\s*,\\s*/)\n\treturn {\n\t\tid: song.copyrightId,\n\t\tname: song.title,\n\t\talbum: {id: song.albumId, name: song.albumName},\n\t\tartists: singerId.map((id, index) => ({id, name: singerName[index]}))\n\t}\n}\n\nconst search = info => {\n\tconst url =\n\t\t'http://m.music.migu.cn/migu/remoting/scr_search_tag?' +\n\t\t'keyword=' + encodeURIComponent(info.keyword) + '&type=2&rows=20&pgc=1'\n\n\treturn request('GET', url)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst list = ((jsonBody || {}).musics || []).map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\nconst single = (id, format) => {\n\tconst url =\n\t\t'http://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?' +\n\t\t'dataType=2&' + crypto.miguapi.encryptBody({copyrightId: id.toString(), type: format})\n\n\treturn request('GET', url, headers)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst {playUrl} = jsonBody.data\n\t\treturn playUrl ? encodeURI(playUrl) : Promise.reject()\n\t})\n}\n\nconst track = id =>\n\tPromise.all(\n\t\t[3, 2, 1].slice(select.ENABLE_FLAC ? 0 : 1)\n\t\t.map(format => single(id, format).catch(() => null))\n\t)\n\t.then(result => result.find(url => url) || Promise.reject())\n\t.catch(() => insure().migu.track(id))\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, track}"
  },
  {
    "path": "src/provider/netease.js",
    "content": "const cache = require('../cache')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst search = info => {\n\tconst url =\n\t\t'http://music.163.com/api/album/' + info.album.id\n\n\treturn request('GET', url)\n\t.then(response => response.body())\n\t.then(body => {\n\t\tconst jsonBody = JSON.parse(body.replace(/\"dfsId\":(\\d+)/g, '\"dfsId\":\"$1\"')) // for js precision\n\t\tconst matched = jsonBody.album.songs.find(song => song.id === info.id)\n\t\tif (matched)\n\t\t\treturn matched.hMusic.dfsId || matched.mMusic.dfsId || matched.lMusic.dfsId\n\t\telse\n\t\t\treturn Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tif (!id || id === '0') return Promise.reject()\n\treturn crypto.uri.retrieve(id)\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check}"
  },
  {
    "path": "src/provider/qq.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst request = require('../request')\n\nconst headers = {\n\t'origin': 'http://y.qq.com/',\n\t'referer': 'http://y.qq.com/',\n\t'cookie': process.env.QQ_COOKIE || null // 'uin=; qm_keyst=',\n}\n\nconst playable = song => {\n\tconst switchFlag = song['switch'].toString(2).split('')\n\tswitchFlag.pop()\n\tswitchFlag.reverse()\n\tconst playFlag = switchFlag[0]\n\tconst tryFlag = switchFlag[13]\n\treturn ((playFlag == 1) || ((playFlag == 1) && (tryFlag == 1)))\n}\n\nconst format = song => ({\n\tid: {song: song.mid, file: song.file.media_mid},\n\tname: song.name,\n\tduration: song.interval * 1000,\n\talbum: {id: song.album.mid, name: song.album.name},\n\tartists: song.singer.map(({mid, name}) => ({id: mid, name}))\n})\n\nconst search = info => {\n\tconst url =\n\t\t'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' +\n\t\t'ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&' +\n\t\t'searchid=46804741196796149&t=0&aggr=1&cr=1&catZhida=1&lossless=0&' +\n\t\t'flag_qc=0&p=1&n=20&w=' + encodeURIComponent(info.keyword) + '&' +\n\t\t'g_tk=5381&jsonpCallback=MusicJsonCallback10005317669353331&loginUin=0&hostUin=0&' +\n\t\t'format=jsonp&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0'\n\n\treturn request('GET', url)\n\t.then(response => response.jsonp())\n\t.then(jsonBody => {\n\t\tconst list = jsonBody.data.song.list.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\nconst single = (id, format) => {\n\t// const classic = ['001yS0N33yPm1B', '000bog5B2DYgHN', '002bongo1BDtKz', '004RDW5Q2ol2jj', '001oEME64eXNbp', '001e9dH11YeXGp', '0021onBk2QNjBu', '001YoUs11jvsIK', '000SNxc91Mw3UQ', '002k94ea4379uy']\n\t// id = id || classic[Math.floor(classic.length * Math.random())]\n\tconst uin = ((headers.cookie || '').match(/uin=(\\d+)/) || [])[1] || '0'\n\n\tconst concatenate = vkey => {\n\t\tif (!vkey) return Promise.reject()\n\t\tconst host = ['streamoc.music.tc.qq.com', 'mobileoc.music.tc.qq.com', 'isure.stream.qqmusic.qq.com', 'dl.stream.qqmusic.qq.com', 'aqqmusic.tc.qq.com/amobile.music.tc.qq.com'][3]\n\t\treturn `http://${host}/${format.join(id.file)}?vkey=${vkey}&uin=0&fromtag=8&guid=7332953645`\n\t}\n\n\t// const url =\n\t// \t'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' +\n\t// \t'?g_tk=0&loginUin=0&hostUin=0&format=json&inCharset=utf8' +\n\t// \t'&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0' +\n\t// \t'&cid=205361747&uin=0&guid=7332953645' +\n\t// \t'&songmid='+ id.song + '&filename='+ format.join(id.file)\n\n\t// return request('GET', url, headers)\n\t// .then(response => response.json())\n\t// .then(jsonBody => {\n\t// \tconst {vkey} = jsonBody.data.items[0]\n\t// \treturn concatenate(vkey)\n\t// })\n\n\tconst url =\n\t\t'https://u.y.qq.com/cgi-bin/musicu.fcg?data=' +\n\t\tencodeURIComponent(JSON.stringify({\n\t\t\t// req: {\n\t\t\t// \tmethod: 'GetCdnDispatch',\n\t\t\t// \tmodule: 'CDN.SrfCdnDispatchServer',\n\t\t\t// \tparam: {\n\t\t\t// \t\tcalltype: 0,\n\t\t\t// \t\tguid: '7332953645',\n\t\t\t// \t\tuserip: ''\n\t\t\t// \t}\n\t\t\t// },\n\t\t\treq_0: {\n\t\t\t\tmodule: 'vkey.GetVkeyServer',\n\t\t\t\tmethod: 'CgiGetVkey',\n\t\t\t\tparam: {\n\t\t\t\t\tguid: '7332953645',\n\t\t\t\t\tloginflag: 1,\n\t\t\t\t\tfilename: [format.join(id.file)],\n\t\t\t\t\tsongmid: [id.song],\n\t\t\t\t\tsongtype: [0],\n\t\t\t\t\tuin,\n\t\t\t\t\tplatform: '20'\n\t\t\t\t}\n\t\t\t}\n\t\t}))\n\n\treturn request('GET', url, headers)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst { sip, midurlinfo } = jsonBody.req_0.data\n\t\t// const vkey =\n\t\t// \tjsonBody.req_0.data.midurlinfo[0].vkey ||\n\t\t// \t(jsonBody.req_0.data.testfile2g.match(/vkey=(\\w+)/) || [])[1]\n\t\t// return concatenate(vkey)\n\t\treturn midurlinfo[0].purl ? sip[0] + midurlinfo[0].purl : Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tid.key = id.file\n\treturn Promise.all(\n\t\t[['F000', '.flac'], ['M800', '.mp3'], ['M500', '.mp3']].slice((headers.cookie || typeof(window) !== 'undefined') ? (select.ENABLE_FLAC ? 0 : 1) : 2)\n\t\t.map(format => single(id, format).catch(() => null))\n\t)\n\t.then(result => result.find(url => url) || Promise.reject())\n\t.catch(() => insure().qq.track(id))\n\n\t// return request(\n\t// \t'POST', 'http://acc.music.qq.com/base/fcgi-bin/fcg_music_express_mobile2.fcg', {},\n\t// \t`<root>\n\t// \t\t<uid></uid><sid></sid><v>90</v><cv>70003</cv><ct>19</ct><OpenUDID>0</OpenUDID>\n\t// \t\t<mcc>460</mcc><mnc>01</mnc><chid>001</chid><webp>0</webp><gray>0</gray><patch>105</patch>\n\t// \t\t<jailbreak>0</jailbreak><nettype>2</nettype><qq>12345678</qq><authst></authst><localvip>2</localvip>\n\t// \t\t<cid>352</cid><platform>ios</platform><musicname>M800${id}.mp3</musicname><downloadfrom>0</downloadfrom>\n\t// \t</root>`.replace(/\\s/, '')\n\t// )\n\t// .then(response => response.body(true))\n\t// .then(body => {\n\t// \tconst xml = require('zlib').inflateSync(body.slice(5)).toString()\n\t// \tconst focus = xml.match(/<item name=\"(.+)\">(.+)<\\/item>/)\n\t// \treturn `http://streamoc.music.tc.qq.com/${focus[1]}?vkey=${focus[2]}&guid=0&uin=12345678&fromtag=6`\n\t// })\n\n\t// const url =\n\t// \t'https://i.y.qq.com/v8/playsong.html?ADTAG=newyqq.song&songmid=' + id\n\n\t// const mobile = {'user-agent': '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'}\n\t// return request('GET', url, mobile)\n\t// .then(response => response.body())\n\t// .then(body => {\n\t// \tconst audio = body.match(/<audio[^>]+src=\"([^\"]+)\"[^>]*>/)\n\t// \tif (audio)\n\t// \t\treturn audio[1].replace(/C400(\\w+)\\.m4a/, 'M500$1.mp3')\n\t// \telse\n\t// \t\treturn Promise.reject()\n\t// })\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, track}"
  },
  {
    "path": "src/provider/select.js",
    "content": "module.exports = list => list[0]\n\nmodule.exports.ENABLE_FLAC = (process.env.ENABLE_FLAC || '').toLowerCase() === 'true'"
  },
  {
    "path": "src/provider/xiami.js",
    "content": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst headers = {\n\t// 'origin': 'http://www.xiami.com/',\n\t// 'referer': 'http://www.xiami.com/'\n\t'referer': 'https://h.xiami.com/'\n}\n\nconst format = song => ({\n\tid: song.song_id,\n\tname: song.song_name,\n\talbum: {id: song.album_id, name: song.album_name},\n\tartists: [{id: song.artist_id, name: song.artist_name}]\n})\n\nconst caesar = pattern => {\n\tconst height = parseInt(pattern[0])\n\tpattern = pattern.slice(1)\n\tconst width = Math.ceil(pattern.length / height)\n\tconst unpad = height - (width * height - pattern.length)\n\n\tconst matrix = Array.from(Array(height).keys()).map(i =>\n\t\tpattern.slice(i < unpad ? i * width : unpad * width + (i - unpad) * (width - 1)).slice(0, i < unpad ? width : width - 1)\n\t)\n\n\tconst transpose = Array.from(Array(width).keys()).map(x =>\n\t\tArray.from(Array(height).keys()).map(y => matrix[y][x]).join('')\n\t)\n\n\treturn unescape(transpose.join('')).replace(/\\^/g, '0')\n}\n\nconst token = () => {\n\treturn request('GET', 'https://www.xiami.com')\n\t.then(response =>\n\t\tresponse.headers['set-cookie'].map(line => line.replace(/;.+$/, '')).reduce(\n\t\t\t(cookie, line) => (line = line.split(/\\s*=\\s*/).map(decodeURIComponent), Object.assign(cookie, {[line[0]]: line[1]})), {}\n\t\t)\n\t)\n}\n\n// const search = info => {\n// \treturn cache(token)\n// \t.then(cookie => {\n// \t\tconst query = JSON.stringify({key: info.keyword, pagingVO: {page: 1, pageSize: 60}})\n// \t\tconst message = cookie['xm_sg_tk'].split('_')[0] + '_xmMain_/api/search/searchSongs_' + query\n// \t\treturn request('GET', 'https://www.xiami.com/api/search/searchSongs?_q=' + encodeURIComponent(query) + '&_s=' + crypto.md5.digest(message), {\n// \t\t\treferer: 'https://www.xiami.com/search?key=' + encodeURIComponent(info.keyword),\n// \t\t\tcookie: Object.keys(cookie).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(cookie[key])).join('; ')\n// \t\t})\n// \t\t.then(response => response.json())\n// \t\t.then(jsonBody => {\n// \t\t\tconst matched = jsonBody.result.data.songs[0]\n// \t\t\tif (matched)\n// \t\t\t\treturn matched.songId\n// \t\t\telse\n// \t\t\t\treturn Promise.reject()\n// \t\t})\n// \t})\n// }\n\nconst search = info => {\n\tconst url =\n\t\t'http://api.xiami.com/web?v=2.0&app_key=1' +\n\t\t'&key=' + encodeURIComponent(info.keyword) + '&page=1' +\n\t\t'&limit=20&callback=jsonp&r=search/songs'\n\n\treturn request('GET', url, headers)\n\t.then(response => response.jsonp())\n\t.then(jsonBody => {\n\t\tconst list = jsonBody.data.songs.map(format)\n\t\tconst matched = select(list, info)\n\t\treturn matched ? matched.id : Promise.reject()\n\t})\n}\n\n// const track = id => {\n// \tconst url =\n// \t\t'https://emumo.xiami.com/song/playlist/id/' + id +\n// \t\t'/object_name/default/object_id/0/cat/json'\n\n// \treturn request('GET', url, headers)\n// \t.then(response => response.json())\n// \t.then(jsonBody => {\n// \t\tif (jsonBody.data.trackList == null) {\n// \t\t\treturn Promise.reject()\n// \t\t}\n// \t\telse {\n// \t\t\tconst location = jsonBody.data.trackList[0].location\n// \t\t\tconst songUrl = 'http:' + caesar(location)\n// \t\t\treturn songUrl\n// \t\t}\n// \t})\n// \t.then(origin => {\n// \t\tconst updated = origin.replace('m128', 'm320')\n// \t\treturn request('HEAD', updated)\n// \t\t.then(response => response.statusCode == 200 ? updated : origin)\n// \t\t.catch(() => origin)\n// \t})\n// \t.catch(() => insure().xiami.track(id))\n// }\n\nconst track = id => {\n\tconst url =\n\t\t'https://api.xiami.com/web?v=2.0&app_key=1' +\n\t\t'&id=' + id + '&callback=jsonp&r=song/detail'\n\n\treturn request('GET', url, headers)\n\t.then(response => response.jsonp())\n\t.then(jsonBody =>\n\t\tjsonBody.data.song.listen_file || Promise.reject()\n\t)\n\t.catch(() => insure().xiami.track(id))\n}\n\nconst check = info => cache(search, info).then(track)\n\nmodule.exports = {check, track}"
  },
  {
    "path": "src/provider/youtube.js",
    "content": "const cache = require('../cache')\nconst request = require('../request')\nconst parse = query => (query || '').split('&').reduce((result, item) => (item = item.split('=').map(decodeURIComponent), Object.assign({}, result, {[item[0]]: item[1]})), {})\n\n// const proxy = require('url').parse('http://127.0.0.1:1080')\nconst proxy = undefined\nconst key = process.env.YOUTUBE_KEY || null // YouTube Data API v3\n\nconst signature = (id = '-tKVN2mAKRI') => {\n\tconst url =\n\t\t`https://www.youtube.com/watch?v=${id}`\n\n\treturn request('GET', url, {}, null, proxy)\n\t.then(response => response.body())\n\t.then(body => {\n\t\tlet assets = /\"assets\":{[^}]+}/.exec(body)[0]\n\t\tassets = JSON.parse(`{${assets}}`).assets\n\t\treturn request('GET', 'https://youtube.com' + assets.js, {}, null, proxy).then(response => response.body())\n\t})\n\t.then(body => {\n\t\tconst [_, funcArg, funcBody] = /function\\((\\w+)\\)\\s*{([^}]+split\\(\"\"\\)[^}]+join\\(\"\"\\))};/.exec(body)\n\t\tconst helperName = /;(.+?)\\..+?\\(/.exec(funcBody)[1]\n\t\tconst helperContent = new RegExp(`var ${helperName}={[\\\\s\\\\S]+?};`).exec(body)[0]\n\t\treturn new Function([funcArg], helperContent + '\\n' + funcBody)\n\t})\n}\n\nconst apiSearch = info => {\n\tconst url =\n\t\t`https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(info.keyword)}&type=video&key=${key}`\n\n\treturn request('GET', url, {accept: 'application/json'}, null, proxy)\n\t.then(response => response.json())\n\t.then(jsonBody => {\n\t\tconst matched = jsonBody.items[0]\n\t\tif (matched)\n\t\t\treturn matched.id.videoId\n\t\telse\n\t\t\treturn Promise.reject()\n\t})\n}\n\nconst search = info => {\n\tconst url =\n\t\t`https://www.youtube.com/results?search_query=${encodeURIComponent(info.keyword)}`\n\n\treturn request('GET', url, {}, null, proxy)\n\t.then(response => response.body())\n\t.then(body => {\n\t\tconst initialData = JSON.parse(body.match(/window\\[\"ytInitialData\"\\]\\s*=\\s*([^;]+);/)[1])\n\t\tconst matched = initialData.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]\n\t\tif (matched)\n\t\t\treturn matched.videoRenderer.videoId\n\t\telse\n\t\t\treturn Promise.reject()\n\t})\n}\n\nconst track = id => {\n\tconst url =\n\t\t`https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage`\n\n\treturn request('GET', url, {}, null, proxy)\n\t.then(response => response.body())\n\t.then(body => JSON.parse(parse(body).player_response).streamingData)\n\t.then(streamingData => {\n\t\tconst stream = streamingData.formats.concat(streamingData.adaptiveFormats)\n\t\t.find(format => format.itag === 140)\n\t\t// .filter(format => [249, 250, 140, 251].includes(format.itag)) // NetaseMusic PC client do not support webm format\n\t\t// .sort((a, b) => b.bitrate - a.bitrate)[0]\n\t\tconst target = parse(stream.signatureCipher)\n\t\treturn stream.url || (target.sp.includes('sig') ? cache(signature, undefined, 24 * 60 * 60 * 1000).then(sign => target.url + '&sig=' + sign(target.s)) : target.url)\n\t})\n}\n\nconst check = info => cache(key ? apiSearch : search, info).then(track)\n\nmodule.exports = {check, track}\n"
  },
  {
    "path": "src/request.js",
    "content": "const zlib = require('zlib')\nconst http = require('http')\nconst https = require('https')\nconst parse = require('url').parse\n\nconst translate = host => (global.hosts || {})[host] || host\n\nconst create = (url, proxy) => (((typeof(proxy) === 'undefined' ? global.proxy : proxy) || url).protocol === 'https:' ? https : http).request\n\nconst configure = (method, url, headers, proxy) => {\n\theaders = headers || {}\n\tproxy = typeof(proxy) === 'undefined' ? global.proxy : proxy\n\tif ('content-length' in headers) delete headers['content-length']\n\n\tconst options = {}\n\toptions._headers = headers\n\tif (proxy && url.protocol === 'https:') {\n\t\toptions.method = 'CONNECT'\n\t\toptions.headers = Object.keys(headers).reduce((result, key) => Object.assign(result, ['host', 'user-agent'].includes(key) && {[key]: headers[key]}), {})\n\t}\n\telse {\n\t\toptions.method = method\n\t\toptions.headers = headers\n\t}\n\n\tif (proxy) {\n\t\toptions.hostname = translate(proxy.hostname)\n\t\toptions.port = proxy.port || ((proxy.protocol === 'https:') ? 443 : 80)\n\t\toptions.path = (url.protocol === 'https:') ? (translate(url.hostname) + ':' + (url.port || 443)) : ('http://' + translate(url.hostname) + url.path) \n\t}\n\telse {\n\t\toptions.hostname = translate(url.hostname)\n\t\toptions.port = url.port || ((url.protocol === 'https:') ? 443 : 80)\n\t\toptions.path = url.path\n\t}\n\treturn options\n}\n\nconst request = (method, url, headers, body, proxy) => {\n\turl = parse(url)\n\theaders = headers || {}\n\tconst options = configure(method, url, Object.assign({\n\t\t'host': url.hostname,\n\t\t'accept': 'application/json, text/plain, */*',\n\t\t'accept-encoding': 'gzip, deflate',\n\t\t'accept-language': 'zh-CN,zh;q=0.9',\n\t\t'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36'\n\t}, headers), proxy)\n\n\treturn new Promise((resolve, reject) => {\n\t\tcreate(url, proxy)(options)\n\t\t.on('response', response => resolve(response))\n\t\t.on('connect', (_, socket) =>\n\t\t\thttps.request({\n\t\t\t\tmethod: method,\n\t\t\t\tpath: url.path,\n\t\t\t\theaders: options._headers,\n\t\t\t\tsocket: socket,\n\t\t\t\tagent: false\n\t\t\t})\n\t\t\t.on('response', response => resolve(response))\n\t\t\t.on('error', error => reject(error))\n\t\t\t.end(body)\n\t\t)\n\t\t.on('error', error => reject(error))\n\t\t.end(options.method.toUpperCase() === 'CONNECT' ? undefined : body)\n\t})\n\t.then(response => {\n\t\tif (new Set([201, 301, 302, 303, 307, 308]).has(response.statusCode))\n\t\t\treturn request(method, url.resolve(response.headers.location || url.href), (delete headers.host, headers), body, proxy)\n\t\telse\n\t\t\treturn Object.assign(response, {url: url, body: raw => read(response, raw), json: () => json(response), jsonp: () => jsonp(response)})\n\t})\n}\n\nconst read = (connect, raw) =>\n\tnew Promise((resolve, reject) => {\n\t\tconst chunks = []\n\t\tconnect\n\t\t.on('data', chunk => chunks.push(chunk))\n\t\t.on('end', () => resolve(Buffer.concat(chunks)))\n\t\t.on('error', error => reject(error))\n\t})\n\t.then(buffer => {\n\t\tbuffer = (buffer.length && ['gzip', 'deflate'].includes(connect.headers['content-encoding'])) ? zlib.unzipSync(buffer) : buffer\n\t\treturn raw ? buffer : buffer.toString()\n\t})\n\nconst json = connect => read(connect, false).then(body => JSON.parse(body))\nconst jsonp = connect => read(connect, false).then(body => JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length)))\n\nrequest.read = read\nrequest.create = create\nrequest.translate = translate\nrequest.configure = configure\n\nmodule.exports = request"
  },
  {
    "path": "src/server.js",
    "content": "const fs = require('fs')\nconst net = require('net')\nconst path = require('path')\nconst parse = require('url').parse\n\nconst sni = require('./sni')\nconst hook = require('./hook')\nconst request = require('./request')\n\nconst proxy = {\n\tcore: {\n\t\tmitm: (req, res) => {\n\t\t\tif (req.url == '/proxy.pac') {\n\t\t\t\tconst url = parse('http://' + req.headers.host)\n\t\t\t\tres.writeHead(200, {'Content-Type': 'application/x-ns-proxy-autoconfig'})\n\t\t\t\tres.end(`\n\t\t\t\t\tfunction FindProxyForURL(url, host) {\n\t\t\t\t\t\tif (${Array.from(hook.target.host).map(host => (`host == '${host}'`)).join(' || ')}) {\n\t\t\t\t\t\t\treturn 'PROXY ${url.hostname}:${url.port || 80}'\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn 'DIRECT'\n\t\t\t\t\t}\n\t\t\t\t`)\n\t\t\t}\n\t\t\telse {\n\t\t\t\tconst ctx = {res, req}\n\t\t\t\tPromise.resolve()\n\t\t\t\t.then(() => proxy.protect(ctx))\n\t\t\t\t.then(() => proxy.authenticate(ctx))\n\t\t\t\t.then(() => hook.request.before(ctx))\n\t\t\t\t.then(() => proxy.filter(ctx))\n\t\t\t\t.then(() => proxy.log(ctx))\n\t\t\t\t.then(() => proxy.mitm.request(ctx))\n\t\t\t\t.then(() => hook.request.after(ctx))\n\t\t\t\t.then(() => proxy.mitm.response(ctx))\n\t\t\t\t.catch(() => proxy.mitm.close(ctx))\n\t\t\t}\n\t\t},\n\t\ttunnel: (req, socket, head) => {\n\t\t\tconst ctx = {req, socket, head}\n\t\t\tPromise.resolve()\n\t\t\t.then(() => proxy.protect(ctx))\n\t\t\t.then(() => proxy.authenticate(ctx))\n\t\t\t.then(() => hook.connect.before(ctx))\n\t\t\t.then(() => proxy.filter(ctx))\n\t\t\t.then(() => proxy.log(ctx))\n\t\t\t.then(() => proxy.tunnel.connect(ctx))\n\t\t\t.then(() => proxy.tunnel.dock(ctx))\n\t\t\t.then(() => hook.negotiate.before(ctx))\n\t\t\t.then(() => proxy.tunnel.pipe(ctx))\n\t\t\t.catch(() => proxy.tunnel.close(ctx))\n\t\t}\n\t},\n\tabort: (socket, from) => {\n\t\t// console.log('call abort', from)\n\t\tif (socket) socket.end()\n\t\tif (socket && !socket.destroyed) socket.destroy()\n\t},\n\tprotect: ctx => {\n\t\tconst {req, res, socket} = ctx\n\t\tif (req) req.on('error', () => proxy.abort(req.socket, 'req'))\n\t\tif (res) res.on('error', () => proxy.abort(res.socket, 'res'))\n\t\tif (socket) socket.on('error', () => proxy.abort(socket, 'socket'))\n\t},\n\tlog: ctx => {\n\t\tconst {req, socket, decision} = ctx\n\t\tconst mark = {close: '|', blank: '-', proxy: '>'}[decision] || '>'\n\t\tif (socket)\n\t\t\tconsole.log('TUNNEL', mark, req.url)\n\t\telse\n\t\t\tconsole.log('MITM', mark, parse(req.url).host, req.socket.encrypted ? '(ssl)' : '')\n\t},\n\tauthenticate: ctx => {\n\t\tconst {req, res, socket} = ctx\n\t\tconst credential = Buffer.from((req.headers['proxy-authorization'] || '').split(/\\s+/).pop() || '', 'base64').toString()\n\t\tif ('proxy-authorization' in req.headers) delete req.headers['proxy-authorization']\n\t\tif (server.authentication && credential != server.authentication && (socket || req.url.startsWith('http://'))) {\n\t\t\tif (socket)\n\t\t\t\tsocket.write('HTTP/1.1 407 Proxy Auth Required\\r\\nProxy-Authenticate: Basic realm=\"realm\"\\r\\n\\r\\n')\n\t\t\telse\n\t\t\t\tres.writeHead(407, {'proxy-authenticate': 'Basic realm=\"realm\"'})\n\t\t\treturn Promise.reject(ctx.error = 'authenticate')\n\t\t}\n\t},\n\tfilter: ctx => {\n\t\tif (ctx.decision || ctx.req.local) return\n\t\tconst url = parse((ctx.socket ? 'https://' : '') + ctx.req.url)\n\t\tconst match = pattern => url.href.search(new RegExp(pattern, 'g')) != -1\n\t\ttry {\n\t\t\tconst allow = server.whitelist.some(match)\n\t\t\tconst deny = server.blacklist.some(match)\n\t\t\t// console.log('allow', allow, 'deny', deny)\n\t\t\tif (!allow && deny) {\n\t\t\t\treturn Promise.reject(ctx.error = 'filter')\n\t\t\t}\n\t\t}\n\t\tcatch(error) {\n\t\t\tctx.error = error\n\t\t}\n\t},\n\tmitm: {\n\t\trequest: ctx => new Promise((resolve, reject) => {\n\t\t\tif (ctx.decision === 'close') return reject(ctx.error = ctx.decision)\n\t\t\tconst {req} = ctx\n\t\t\tconst url = parse(req.url)\n\t\t\tconst options = request.configure(req.method, url, req.headers)\n\t\t\tctx.proxyReq = request.create(url)(options)\n\t\t\t.on('response', proxyRes => resolve(ctx.proxyRes = proxyRes))\n\t\t\t.on('error', error => reject(ctx.error = error))\n\t\t\treq.readable ? req.pipe(ctx.proxyReq) : ctx.proxyReq.end(req.body)\n\t\t}),\n\t\tresponse: ctx => {\n\t\t\tconst {res, proxyRes} = ctx\n\t\t\tproxyRes.on('error', () => proxy.abort(proxyRes.socket, 'proxyRes'))\n\t\t\tres.writeHead(proxyRes.statusCode, proxyRes.headers)\n\t\t\tproxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body)\n\t\t},\n\t\tclose: ctx => {\n\t\t\tproxy.abort(ctx.res.socket, 'mitm')\n\t\t}\n\t},\n\ttunnel: {\n\t\tconnect: ctx => new Promise((resolve, reject) => {\n\t\t\tif (ctx.decision === 'close') return reject(ctx.error = ctx.decision)\n\t\t\tconst {req} = ctx\n\t\t\tconst url = parse('https://' + req.url)\n\t\t\tif (global.proxy && !req.local) {\n\t\t\t\tconst options = request.configure(req.method, url, req.headers)\n\t\t\t\trequest.create(proxy)(options)\n\t\t\t\t.on('connect', (_, proxySocket) => resolve(ctx.proxySocket = proxySocket))\n\t\t\t\t.on('error', error => reject(ctx.error = error))\n\t\t\t\t.end()\n\t\t\t}\n\t\t\telse {\n\t\t\t\tconst proxySocket = net.connect(url.port || 443, request.translate(url.hostname))\n\t\t\t\t.on('connect', () => resolve(ctx.proxySocket = proxySocket))\n\t\t\t\t.on('error', error => reject(ctx.error = error))\n\t\t\t}\n\t\t}),\n\t\tdock: ctx => new Promise(resolve => {\n\t\t\tconst {req, head, socket} = ctx\n\t\t\tsocket\n\t\t\t.once('data', data => resolve(ctx.head = Buffer.concat([head, data])))\n\t\t\t.write(`HTTP/${req.httpVersion} 200 Connection established\\r\\n\\r\\n`)\n\t\t}).then(data => ctx.socket.sni = sni(data)).catch(() => {}),\n\t\tpipe: ctx => {\n\t\t\tif (ctx.decision === 'blank') return Promise.reject(ctx.error = ctx.decision)\n\t\t\tconst {head, socket, proxySocket} = ctx\n\t\t\tproxySocket.on('error', () => proxy.abort(ctx.proxySocket, 'proxySocket'))\n\t\t\tproxySocket.write(head)\n\t\t\tsocket.pipe(proxySocket)\n\t\t\tproxySocket.pipe(socket)\n\t\t},\n\t\tclose: ctx => {\n\t\t\tproxy.abort(ctx.socket, 'tunnel')\n\t\t}\n\t}\n}\n\nconst options = {\n\tkey: fs.readFileSync(path.join(__dirname, '..', 'server.key')),\n\tcert: fs.readFileSync(path.join(__dirname, '..', 'server.crt'))\n}\n\nconst server = {\n\thttp: require('http').createServer().on('request', proxy.core.mitm).on('connect', proxy.core.tunnel),\n\thttps: require('https').createServer(options).on('request', proxy.core.mitm).on('connect', proxy.core.tunnel)\n}\n\nserver.whitelist = []\nserver.blacklist = ['://127\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+', '://localhost']\nserver.authentication = null\n\nmodule.exports = server"
  },
  {
    "path": "src/sni.js",
    "content": "// Thanks to https://github.com/buschtoens/sni\n\nmodule.exports = data => {\n\tlet end = data.length\n\tlet pointer = 5 + 1 + 3 + 2 + 32\n\tconst nan = (number = pointer) => isNaN(number)\n\n\tif (pointer + 1 > end || nan()) return null\n\tpointer += 1 + data[pointer]\n\n\tif (pointer + 2 > end || nan()) return null\n\tpointer += 2 + data.readInt16BE(pointer)\n\n\tif (pointer + 1 > end || nan()) return null\n\tpointer += 1 + data[pointer]\n\n\tif (pointer + 2 > end || nan()) return null\n\tconst extensionsLength = data.readInt16BE(pointer)\n\tpointer += 2\n\tconst extensionsEnd = pointer + extensionsLength\n\n\tif (extensionsEnd > end || nan(extensionsEnd)) return null\n\tend = extensionsEnd\n\n\twhile (pointer + 4 <= end || nan()) {\n\t\tconst extensionType = data.readInt16BE(pointer)\n\t\tconst extensionSize = data.readInt16BE(pointer + 2)\n\t\tpointer += 4\n\t\tif (extensionType !== 0) {\n\t\t\tpointer += extensionSize\n\t\t\tcontinue\n\t\t}\n\t\tif (pointer + 2 > end || nan()) return null\n\t\tconst nameListLength = data.readInt16BE(pointer)\n\t\tpointer += 2\n\t\tif (pointer + nameListLength > end) return null\n\n\t\twhile (pointer + 3 <= end || nan()) {\n\t\t\tconst nameType = data[pointer]\n\t\t\tconst nameLength = data.readInt16BE(pointer + 1)\n\t\t\tpointer += 3\n\t\t\tif (nameType !== 0) {\n\t\t\t\tpointer += nameLength\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (pointer + nameLength > end || nan()) return null\n\t\t\treturn data.toString('ascii', pointer, pointer + nameLength)\n\t\t}\n\t}\n\n\treturn null\n}"
  }
]