Full Code of nondanee/UnblockNeteaseMusic for AI

master b8d5f4b4fe59 cached
47 files
99.1 KB
34.8k tokens
1 symbols
1 requests
Download .txt
Repository: nondanee/UnblockNeteaseMusic
Branch: master
Commit: b8d5f4b4fe59
Files: 47
Total size: 99.1 KB

Directory structure:
gitextract_m6vtz5ok/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.js
├── bridge.js
├── ca.crt
├── docker-compose.yml
├── endpoint.worker.js
├── package.json
├── server.crt
├── server.key
└── src/
    ├── app.js
    ├── bridge.js
    ├── browser/
    │   ├── README.md
    │   ├── background.html
    │   ├── background.js
    │   ├── convert.js
    │   ├── crypto.js
    │   ├── inject.js
    │   ├── manifest.json
    │   ├── request.js
    │   └── script.js
    ├── cache.js
    ├── cli.js
    ├── crypto.js
    ├── hook.js
    ├── kwDES.js
    ├── provider/
    │   ├── baidu.js
    │   ├── find.js
    │   ├── insure.js
    │   ├── joox.js
    │   ├── kugou.js
    │   ├── kuwo.js
    │   ├── match.js
    │   ├── migu.js
    │   ├── netease.js
    │   ├── qq.js
    │   ├── select.js
    │   ├── xiami.js
    │   └── youtube.js
    ├── request.js
    ├── server.js
    └── sni.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git
.npmignore
.gitignore
.dockerignore

LICENSE
*.md

node_modules
npm-debug.log

Dockerfile*
*.yml

src/browser/
ca.*
*.worker.js

================================================
FILE: .github/workflows/publish.yml
================================================
name: publish

on:
  push:
    tags:
       - '*'

jobs:
  docker:
    runs-on: ubuntu-latest
    env:
      REPOSITORY: unblockneteasemusic
      DOCKER_USERNAME: nondanee
      DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
    steps:
      -
        name: Prepare
        id: prepare
        run: |
          ARCH=(amd64 arm/v6 arm/v7 arm64 386 ppc64le s390x)
          PLATFORM=$(printf ",linux/%s" "${ARCH[@]}")
          echo ::set-output name=build_platform::${PLATFORM:1}
          echo ::set-output name=image_name::${DOCKER_USERNAME}/${REPOSITORY}
      -
        name: Checkout
        uses: actions/checkout@v2
      -
        name: Setup Buildx
        uses: crazy-max/ghaction-docker-buildx@v1
      -
        name: Login
        run: |
          echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
      -
        name: Build
        run: |
          docker buildx build \
            --tag ${{ steps.prepare.outputs.image_name }} \
            --platform ${{ steps.prepare.outputs.build_platform }} \
            --output "type=image,push=true" \
            --file Dockerfile .
      -
        name: Check Manifest
        run: |
          docker buildx imagetools inspect ${{ steps.prepare.outputs.image_name }}

  npm:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v2
      -
        name: Setup Node.js
        uses: actions/setup-node@v1
        with:
          registry-url: https://registry.npmjs.org/
      -
        name: Publish
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .gitignore
================================================
# IDE
.vscode
.idea

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

# pkg dist directory
dist/

# es6 transformation
src/browser/provider
src/browser/cache.js

================================================
FILE: .npmignore
================================================
.npmignore
.gitignore
.dockerignore

Dockerfile*
*.yml

src/browser/
ca.*
*.worker.js

================================================
FILE: Dockerfile
================================================
FROM alpine
RUN apk add --update nodejs npm --repository=http://dl-cdn.alpinelinux.org/alpine/latest-stable/main/

ENV NODE_ENV production

WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --production
COPY . .

EXPOSE 8080 8081

ENTRYPOINT ["node", "app.js"]

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Nzix

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img src="https://user-images.githubusercontent.com/26399680/47980314-0e3f1700-e102-11e8-8857-e3436ecc8beb.png" alt="logo" width="140" height="140" align="right">

# UnblockNeteaseMusic

解锁网易云音乐客户端变灰歌曲

## 特性

- 使用 QQ / 虾米 / 百度 / 酷狗 / 酷我 / 咪咕 / JOOX 音源替换变灰歌曲链接 (默认仅启用一、五、六)
- 为请求增加 `X-Real-IP` 参数解锁海外限制,支持指定网易云服务器 IP,支持设置上游 HTTP / HTTPS 代理
- 完整的流量代理功能 (HTTP / HTTPS),可直接作为系统代理 (同时支持 PAC)

## 运行

使用 npx

```
$ npx @nondanee/unblockneteasemusic
```

或使用 Docker

```
$ docker run nondanee/unblockneteasemusic
```

```
$ docker-compose up
```

### 配置参数

```
$ unblockneteasemusic -h
usage: unblockneteasemusic [-v] [-p port] [-a address] [-u url] [-f host]
                           [-o source [source ...]] [-t token] [-e url] [-s]
                           [-h]

optional arguments:
  -v, --version                   output the version number
  -p port, --port port            specify server port
  -a address, --address address   specify server host
  -u url, --proxy-url url         request through upstream proxy
  -f host, --force-host host      force the netease server ip
  -o source [source ...], --match-order source [source ...]
                                  set priority of sources
  -t token, --token token         set up proxy authentication
  -e url, --endpoint url          replace virtual endpoint with public host
  -s, --strict                    enable proxy limitation
  -h, --help                      output usage information
```

## 使用

**警告:本项目不提供线上 demo,请不要轻易信任使用他人提供的公开代理服务,以免发生安全问题**

**若将服务部署到公网,强烈建议使用严格模式 (此模式下仅放行网易云音乐所属域名的请求) `-s`  限制代理范围 (需使用 PAC 或 hosts),~~或启用 Proxy Authentication `-t <name>:<password>` 设置代理用户名密码~~ (目前密码认证在 Windows 客户端设置和 macOS 系统设置都无法生效,请不要使用),以防代理被他人滥用**

支持 Windows 客户端,UWP 客户端,Android 客户端,Linux 客户端 (1.2 版本以上需要自签证书 MITM,启动客户端需要增加 `--ignore-certificate-errors` 参数),macOS 客户端 (726 版本以上需要自签证书),iOS 客户端 (配置 https endpoint 或使用自签证书) 和网页版 (需要自签证书,需要脚本配合)

目前除 UWP 外其它客户端均优先请求 HTTPS 接口,默认配置下本代理对网易云所有 HTTPS API 连接返回空数据,促使客户端降级使用 HTTP 接口 (新版 Linux 客户端和 macOS 客户端已无法降级)

因 UWP 应用存在网络隔离,限制流量发送到本机,若使用的代理在 localhost,或修改的 hosts 指向 localhost,需为 "网易云音乐 UWP" 手动开启 loopback 才能使用,请以**管理员身份**执行命令

```powershell
checknetisolation loopbackexempt -a -n="1F8B0F94.122165AE053F_j2p0p5q0044a6"
```

### 方法 1. 修改 hosts

向 hosts 文件添加两条规则

```
<Server IP> music.163.com
<Server IP> interface.music.163.com
```

> 使用此方法必须监听 80 端口 `-p 80` 
>
> **若在本机运行程序**,请指定网易云服务器 IP `-f xxx.xxx.xxx.xxx` (可在修改 hosts 前通过 `ping music.163.com` 获得) **或** 使用代理 `-u http(s)://xxx.xxx.xxx.xxx:xxx`,以防请求死循环
>
> **Android 客户端下修改 hosts 无法直接使用**,原因和解决方法详见[云音乐安卓又搞事啦](https://jixun.moe/post/netease-android-hosts-bypass/),[安卓免 root 绕过网易云音乐 IP 限制](https://jixun.moe/post/android-block-netease-without-root/)

### 方法 2. 设置代理

PAC 自动代理脚本地址 `http://<Server Name:PORT>/proxy.pac`

全局代理地址填写服务器地址和端口号即可

| 平台    | 基础设置 |
| :------ | :------------------------------- |
| Windows | 设置 > 工具 > 自定义代理 (客户端内) |
| UWP     | Windows 设置 > 网络和 Internet > 代理 |
| Linux   | 系统设置 > 网络 > 网络代理 |
| macOS   | 系统偏好设置 > 网络 > 高级 > 代理 |
| Android | WLAN > 修改网络 > 高级选项 > 代理 |
| iOS     | 无线局域网 > HTTP 代理 > 配置代理 |

> 代理工具和方法有很多请自行探索,欢迎在 issues 讨论

### ✳方法 3. 调用接口

作为依赖库使用

```
$ npm install @nondanee/unblockneteasemusic
```

```javascript
const match = require('@nondanee/unblockneteasemusic')

/** 
 * Set proxy or hosts if needed
 */
global.proxy = require('url').parse('http://127.0.0.1:1080')
global.hosts = {'i.y.qq.com': '59.37.96.220'}

/**
 * Find matching song from other platforms
 * @param {Number} id netease song id
 * @param {Array<String>||undefined} source support qq, xiami, baidu, kugou, kuwo, migu, joox
 * @return {Promise<Object>}
 */
match(418602084, ['qq', 'kuwo', 'migu']).then(console.log)
```

## 效果

#### Windows 客户端

<img src="https://user-images.githubusercontent.com/26399680/60316017-87de8a80-999b-11e9-9381-16d40efbe7f6.png" width="100%">

#### UWP 客户端

<img src="https://user-images.githubusercontent.com/26399680/52215123-5a028780-28ce-11e9-8491-08c4c5dac3b4.png" width="100%">

#### Linux 客户端

<img src="https://user-images.githubusercontent.com/26399680/60316169-18b56600-999c-11e9-8ae5-5cd168b0edae.png" width="100%">

#### macOS 客户端

<img src="https://user-images.githubusercontent.com/26399680/52196035-51418f80-2895-11e9-8f33-78a631cdf151.png" width="100%">

#### Android 客户端

<img src="https://user-images.githubusercontent.com/26399680/57972549-eabd2900-79ce-11e9-8fef-95cb60906298.png" width="50%">

#### iOS 客户端

<img src="https://user-images.githubusercontent.com/26399680/57972440-f90a4580-79cc-11e9-8dbf-6150ee299b9c.jpg" width="50%">

## 致谢

感谢大佬们为逆向 eapi 所做的努力

使用的其它平台音源 API 出自

[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)

[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)

向所有同类项目致敬

[EraserKing/CloudMusicGear](https://github.com/EraserKing/CloudMusicGear)

[EraserKing/Unblock163MusicClient](https://github.com/EraserKing/Unblock163MusicClient)

[ITJesse/UnblockNeteaseMusic](https://github.com/ITJesse/UnblockNeteaseMusic/)

[bin456789/Unblock163MusicClient-Xposed](https://github.com/bin456789/Unblock163MusicClient-Xposed)

[YiuChoi/Unlock163Music](https://github.com/YiuChoi/Unlock163Music)

[yi-ji/NeteaseMusicAbroad](https://github.com/yi-ji/NeteaseMusicAbroad)

[stomakun/NeteaseReverseLadder](https://github.com/stomakun/NeteaseReverseLadder/)

[fengjueming/unblock-NetEaseMusic](https://github.com/fengjueming/unblock-NetEaseMusic)

[acgotaku/NetEaseMusicWorld](https://github.com/acgotaku/NetEaseMusicWorld)

[mengskysama/163-Cloud-Music-Unlock](https://github.com/mengskysama/163-Cloud-Music-Unlock)

[azureplus/163-music-unlock](https://github.com/azureplus/163-music-unlock)

[typcn/163music-mac-client-unlock](https://github.com/typcn/163music-mac-client-unlock)

## 许可

The MIT License

================================================
FILE: app.js
================================================
#!/usr/bin/env node
require('./src/app')

================================================
FILE: bridge.js
================================================
#!/usr/bin/env node
require('./src/bridge')

================================================
FILE: ca.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIJAKX8LdIETDklMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV
BAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb
BgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTE5MDUxODE2MDU0NVoXDTI0
MDUxNjE2MDU0NVowUjELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRl
YXNlTXVzaWMgUm9vdCBDQTEdMBsGA1UECgwUR2l0SHViLmNvbSBAbm9uZGFuZWUw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD23K6Ti2TfLJToCmpCAVgE
Xb8+qTMfrifCpnKlJ+hrL+4KI1j4vSqTOOatqmxGSXZdF/j2kJuI40YThaokcgYx
GFcPcEftSCYGWy8o20u2hzTkkW3KW9wlsDRIXICFXVIsHeSDwz+aVSudkyJHjfaS
aLNb5pPovE7MRj8tDbp55scaSqhEcOe3m1ZlwlCeeXvD7RLKr3xhBKbGEqlJAjFq
RNGzuqylqyJVBLScNHC7Lcf4n6pKr1yPGOeLePOUrIwtj0ynHUcBfeMuCVCsIKL8
vy/oNwlDrZaAMfu5QQslzEf87KY1QgtI6Ppii+tzbmVx1ZxnlaCKqiuwlgBoi/5r
AgMBAAGjUDBOMB0GA1UdDgQWBBRDhbGjnXEUouE9wNFS2k9PtgYYjDAfBgNVHSME
GDAWgBRDhbGjnXEUouE9wNFS2k9PtgYYjDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQDRUh5+JFLEALXQkhPfwrVf4sCXTwLMwVujTPo3NMbhpWiP4cnn
XHGCD5V57bBwjeD6NSrczDIdnN9uTJyFmLNVFMZBguEIeZfLUJLJ6w1ZhfgciX1D
9djyyo6eclkHvi+aPZKfzgMmc5BvHcjyUyS5MzI23kUW6WXUDn3IDIUKrfaH9Mjc
/d4DDZVKQCYrLoBL+XO7pEHUY0u9XZVYWEavQ5tSN8XY1SDrO0yGUpRWET0ltubE
zV7W0LOhuoVCiemboc5H8+njBjCis8obAo1XMmDZzW189L9GPFxHNWlka+KlajZB
tMo90PooZYEOw1rTUrzHb+VZY/tYIAAomGZ0
-----END CERTIFICATE-----


================================================
FILE: docker-compose.yml
================================================
version: '3'

services:
  unblockneteasemusic:
    image: nondanee/unblockneteasemusic
    environment:
      NODE_ENV: production
    ports:
      - 8080:8080


================================================
FILE: endpoint.worker.js
================================================
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

const pattern = /^\/package\/([0-9a-zA-Z_\-=]+)\/(\w+\.\w+)$/

const handleRequest = async request => {
  const notFound = new Response(null, { status: 404 })
  const path = new URL(request.url).pathname
  const [matched, base64Url, fileName] = pattern.exec(path || '') || []
  if (!matched) return notFound
  let url = base64Url.replace(/-/g, '+').replace(/_/g, '/')
  try { url = new URL(atob(url)) } catch(_) { url = null }
  if (!url) return notFound
  const headers = new Headers(request.headers)
  headers.set('host', url.host)
  headers.delete('cookie')
  const { method, body } = request
  return fetch(url, { method, headers, body })
}

================================================
FILE: package.json
================================================
{
  "name": "@nondanee/unblockneteasemusic",
  "version": "0.25.3",
  "description": "Revive unavailable songs for Netease Cloud Music",
  "main": "src/provider/match.js",
  "bin": {
    "unblockneteasemusic": "app.js"
  },
  "scripts": {
    "pkg": "pkg . --out-path=dist/"
  },
  "pkg": {
    "assets": [
      "server.key",
      "server.crt"
    ]
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/nondanee/UnblockNeteaseMusic.git"
  },
  "author": "nondanee",
  "license": "MIT",
  "dependencies": {},
  "publishConfig": {
    "access": "public"
  }
}


================================================
FILE: server.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDkjCCAnqgAwIBAgIJAK/bIUIlE36LMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV
BAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb
BgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTIwMDUxNjE3MTIxM1oXDTIx
MDUxNjE3MTIxM1owezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSww
KgYDVQQKDCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8G
A1UECwwISVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALobECypwEoe8VqM/FJvBRR3p2T+ZWdi
MSPrwfiRJr5p7OMtWBlLveCBV85+R3feidYbQTXlvVTdToY+GN6mFE1x6zG2dvLD
s4UuRnipmvGcFYhIRTX8J4AJiN8VMtW0TNXscRMudpz/FAVtsRrggRaThYg4f/rI
oAPMqKMsS4JoYhxs9ED6E6/tpj3XmSg1ekaXhgacYSYHeyxizZwoOFVCLH3TG5sF
sD6CYNnukYol8bR+VRpvHftIYss5Yz+DyyhYEAMJm1CfQo+xoGR3D0ozbT3hUnzm
fEoOhmSp3sALrFVE4iJSuajoh2/3xhmcyi3xZdWyq4F8hpb+URyaoW0CAwEAAaNC
MEAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwKQYDVR0RBCIwIIINbXVzaWMuMTYzLmNv
bYIPKi5tdXNpYy4xNjMuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQB+zW0o1169aGQI
7GA/8BQ769svpkpdy/lfvkokapFjzoxLTBQhjMo9rqzmGOwr9ksePwQqSDXn685W
mKnEl0CzhBrKnL5x3gHus8bg591xpW+01xAFXSyLITOfMJqMEdY7ylymkm0XZ3aN
vm+yFdP1fr/bZNw6Wrprg3i7eGhj7TdBXRA96usVgBcnCkC1SzEZfnDZsKl9o8Xx
TSOpvzIMSaD7++Bp7BdzA5oCCydv2c++zV/sgCPIr26Jq8UQac+qQP5SMlYyGbAl
vIQRRZyfQ4fPonYDnEPHWFCMyBkQIN39LMhDRsUgn8bT0rnP91xkNAd9S4VWbNDA
5TMiQy3F
-----END CERTIFICATE-----


================================================
FILE: server.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuhsQLKnASh7xWoz8Um8FFHenZP5lZ2IxI+vB+JEmvmns4y1Y
GUu94IFXzn5Hd96J1htBNeW9VN1Ohj4Y3qYUTXHrMbZ28sOzhS5GeKma8ZwViEhF
NfwngAmI3xUy1bRM1exxEy52nP8UBW2xGuCBFpOFiDh/+sigA8yooyxLgmhiHGz0
QPoTr+2mPdeZKDV6RpeGBpxhJgd7LGLNnCg4VUIsfdMbmwWwPoJg2e6RiiXxtH5V
Gm8d+0hiyzljP4PLKFgQAwmbUJ9Cj7GgZHcPSjNtPeFSfOZ8Sg6GZKnewAusVUTi
IlK5qOiHb/fGGZzKLfFl1bKrgXyGlv5RHJqhbQIDAQABAoIBAEmAvtalBMlBh1mY
LV/xcTQwPfDpeOtoILhrOOUPjxnNhD4FfrIe9BNjgmaQAXIadp4VjZ/X6PtHnOfw
RqpJNeOQhq/PvRMMsC59pF+rvQKH/wkgYhV8Ta2IFoLlQHqfB2nGRLKquzYumJ28
QSK4YMOl6CtxBTrrWiemAUTRDdGm8tARiipJH1SEJrS6d/NoRoJx2vixFgD2eS6X
bjnhGjIzvX/w5FWjctqj+SFITP1UI62b6DyWsPOkoosKNteK+Ulz+K6ZFvOx7day
XgUoTcVpwCVr2dVGhJtOrbKPcl1jYCYHJAHwzUZND4x4yftm1mnnsi3bthYqbtHQ
vxLE9YECgYEA9hiZxwiVvLjSe1xT/D75HbB8S1XSnwzpMmqgzStguxCQ0Qg5yiLI
UKRDY8UZvEDV4i2bQGy7mk8lFvX1q2z7Q30+dtT9r2N9a6ujMk5RMfo2BZg/poI6
yDWe2tKUg9cTwfgni4TutLOYkpz3VDPIQHs3k2mpNh7f+8X4RIybDqkCgYEAwZhp
uWMV38Bb0WytswHXL1dRuwBskKqALUBY61dtXkyBuocj8AuRRxfxfZpgJRrHFxDX
O9bQ2nxpVlwKsR6DJDUdxU3+kvwyPfseU5XUBey8WdkuAKD7cKZOHMhFVWccks0U
YJzykNrxB+rGTiwVKa0MOhipuJ7boerwwaN2SyUCgYBP9Ow5o4tq9q3EUNoksZ0k
zUuE+oxlCr/VlplKL9bM0HQMxlxoVWa59LTEfKyA4pvbUbAIfYtydlZ5oE5CdTUp
105tM4R88Jk2W1y5ooJ093OH29CKW/OXSvyi4hpIv592vRa0GOupoFRpBkDBhdWB
RcdnyMOmht+FIOwp8XkLiQKBgAUK3j4Y6ZnxXbLfvMp70soF4TgYs7s05a/IDEjc
9xlMrthX6sS22GrcocqeucBdqS/dnW2Ok9QNB4VbUl/4pnvL8mGQPYBAl2Jr5wdQ
ULxyxRkmAf+8MbBmdIRlZwDpdaIRO2Wk0OCbA0osgEvK9CYovrfIqqsHYDsgbnLs
ugkNAoGBAJok06BN05caPXXLQ2pMwI/7mjcZFjcOMxSloYi7LFkxlyvoTqReAeSa
yOb6W/7obS1X8ms/EAkqiyzJuPtNZJCW/nvV0iCoZ/NxLuyHnFaO344GBAweol+S
Jx0MY8KuDCyeGErc2xdz/yr3ld2PSTq71dhBluGyba2YX+peJ2Yv
-----END RSA PRIVATE KEY-----


================================================
FILE: src/app.js
================================================
#!/usr/bin/env node

const package = require('../package.json')
const config = require('./cli.js')
.program({name: package.name.replace(/@.+\//, ''), version: package.version})
.option(['-v', '--version'], {action: 'version'})
.option(['-p', '--port'], {metavar: 'port', help: 'specify server port'})
.option(['-a', '--address'], {metavar: 'address', help: 'specify server host'})
.option(['-u', '--proxy-url'], {metavar: 'url', help: 'request through upstream proxy'})
.option(['-f', '--force-host'], {metavar: 'host', help: 'force the netease server ip'})
.option(['-o', '--match-order'], {metavar: 'source', nargs: '+', help: 'set priority of sources'})
.option(['-t', '--token'], {metavar: 'token', help: 'set up proxy authentication'})
.option(['-e', '--endpoint'], {metavar: 'url', help: 'replace virtual endpoint with public host'})
.option(['-s', '--strict'], {action: 'store_true', help: 'enable proxy limitation'})
.option(['-h', '--help'], {action: 'help'})
.parse(process.argv)

global.address = config.address
config.port = (config.port || '8080').split(':').map(string => parseInt(string))
const invalid = value => (isNaN(value) || value < 1 || value > 65535)
if (config.port.some(invalid)) {
	console.log('Port must be a number higher than 0 and lower than 65535.')
	process.exit(1)
}
if (config.proxyUrl && !/http(s?):\/\/.+:\d+/.test(config.proxyUrl)) {
	console.log('Please check the proxy url.')
	process.exit(1)
}
if (config.endpoint && !/http(s?):\/\/.+/.test(config.endpoint)) {
	console.log('Please check the endpoint host.')
	process.exit(1)
}
if (config.forceHost && require('net').isIP(config.forceHost) === 0) {
	console.log('Please check the server host.')
	process.exit(1)
}
if (config.matchOrder) {
	const provider = new Set(['netease', 'qq', 'xiami', 'baidu', 'kugou', 'kuwo', 'migu', 'joox', 'youtube'])
	const candidate = config.matchOrder
	if (candidate.some((key, index) => index != candidate.indexOf(key))) {
		console.log('Please check the duplication in match order.')
		process.exit(1)
	}
	else if (candidate.some(key => !provider.has(key))) {
		console.log('Please check the availability of match sources.')
		process.exit(1)
	}
	global.source = candidate
}
if (config.token && !/\S+:\S+/.test(config.token)) {
	console.log('Please check the authentication token.')
	process.exit(1)
}

const parse = require('url').parse
const hook = require('./hook')
const server = require('./server')
const random = array => array[Math.floor(Math.random() * array.length)]
const target = Array.from(hook.target.host)

global.port = config.port
global.proxy = config.proxyUrl ? parse(config.proxyUrl) : null
global.hosts = target.reduce((result, host) => Object.assign(result, {[host]: config.forceHost}), {})
server.whitelist = ['://[\\w.]*music\\.126\\.net', '://[\\w.]*vod\\.126\\.net']
if (config.strict) server.blacklist.push('.*')
server.authentication = config.token || null
global.endpoint = config.endpoint
if (config.endpoint) server.whitelist.push(escape(config.endpoint))

// hosts['music.httpdns.c.163.com'] = random(['59.111.181.35', '59.111.181.38'])
// hosts['httpdns.n.netease.com'] = random(['59.111.179.213', '59.111.179.214'])

const dns = host => new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error ? reject(error) : resolve(records.map(record => record.address))))
const 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), []))
const 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 || []), []))

Promise.all([httpdns, httpdns2].map(query => query(target.join(','))).concat(target.map(dns)))
.then(result => {
	const {host} = hook.target
	result.forEach(array => array.forEach(host.add, host))
	server.whitelist = server.whitelist.concat(Array.from(host).map(escape))
	const log = type => console.log(`${['HTTP', 'HTTPS'][type]} Server running @ http://${address || '0.0.0.0'}:${port[type]}`)
	if (port[0]) server.http.listen(port[0], address).once('listening', () => log(0))
	if (port[1]) server.https.listen(port[1], address).once('listening', () => log(1))
})
.catch(error => console.log(error))


================================================
FILE: src/bridge.js
================================================
#!/usr/bin/env node
const cache = require('./cache')
const parse = require('url').parse
require('./provider/insure').disable = true

const router = {
	qq: require('./provider/qq'),
	xiami: require('./provider/xiami'),
	baidu: require('./provider/baidu'),
	kugou: require('./provider/kugou'),
	kuwo: require('./provider/kuwo'),
	migu: require('./provider/migu'),
	joox: require('./provider/joox')
}

const distribute = (url, router) =>
	Promise.resolve()
	.then(() => {
		const route = url.pathname.slice(1).split('/').map(path => decodeURIComponent(path))
		let pointer = router, argument = decodeURIComponent(url.query)
		try {argument = JSON.parse(argument)} catch(e) {}
		const miss = route.some(path => {
			if (path in pointer) pointer = pointer[path]
			else return true
		})
		if (miss || typeof pointer != 'function') return Promise.reject()
		// return pointer.call(null, argument)
		return cache(pointer, argument, 15 * 60 * 1000)
	})

require('http').createServer()
.listen(parseInt(process.argv[2]) || 9000)
.on('request', (req, res) =>
	distribute(parse(req.url), router)
	.then(data => res.write(data))
	.catch(() => res.writeHead(404))
	.then(() => res.end())
)

================================================
FILE: src/browser/README.md
================================================
# Web Extension Port

For test

## Implementation

- Convert node module to ES6 module which can be directly executed in Chrome 61+ without Babel
- Rewrite crypto module (using CryptoJS) and request (using XMLHttpRequest) module for browser environment
- Do matching in background and transfer result with chrome runtime communication
- Inject content script for hijacking Netease Music Web Ajax response

## Build

```
$ node convert.js
```

## Install

Load unpacked extension in Developer mode

## Known Issue

Audio resources from `kuwo`, `kugou` and `migu` are limited in http protocol only and hence can't load
Most audio resources from `qq` don't support preflight request (OPTIONS) and make playbar buggy

## Reference

- [brix/crypto-js](https://github.com/brix/crypto-js)
- [travist/jsencrypt](https://github.com/travist/jsencrypt)
- [JixunMoe/cuwcl4c](https://github.com/JixunMoe/cuwcl4c)

================================================
FILE: src/browser/background.html
================================================
<script src="https://cdn.jsdelivr.net/npm/crypto-js/crypto-js.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsencrypt/bin/jsencrypt.min.js"></script>
<script type="module" src="background.js"></script>

================================================
FILE: src/browser/background.js
================================================
import match from './provider/match.js'
const self = chrome.runtime.id

chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
	match(request.match, ['qq'])
	.then(song => sendResponse(song))
	.catch(console.log)
	return true
})

chrome.webRequest.onBeforeSendHeaders.addListener(details => {
	let headers = details.requestHeaders
	if(details.url.includes('//music.163.com/')){
		headers.push({name: 'X-Real-IP', value: '118.88.88.88'})
	}
	if(details.initiator == `chrome-extension://${self}`){
		let index = headers.findIndex(item => item.name.toLowerCase() === 'additional-headers')
		if(index === -1) return
		Object.entries(JSON.parse(atob(headers[index].value))).forEach(entry => headers.push({name: entry[0], value: entry[1]}))
		headers.splice(index, 1)
	}
	if(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){
		headers = headers.filter(item => !['referer', 'origin'].includes(item.name.toLowerCase()))
	}
	return {requestHeaders: headers}
}, {urls: ['*://*/*']}, ['blocking', 'requestHeaders', 'extraHeaders'])

chrome.webRequest.onHeadersReceived.addListener(details => {
	let headers = details.responseHeaders
	if(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){
		headers.push({name: 'Access-Control-Allow-Origin', value: '*'})
	}
	return {responseHeaders: headers}
}, {urls: ['*://*/*']}, ['blocking', 'responseHeaders'])

================================================
FILE: src/browser/convert.js
================================================
const fs = require('fs')
const path = require('path')

const importReplacer = (match, state, alias, file) => {
	file = file + (file.endsWith('.js') ? '' : '.js')
	return `import ${alias} from '${file}'`
}

const converter = (input, output, processor) => {
	let data = fs.readFileSync(input).toString()
	if(processor){
		data = processor(data)
	}
	else{
		data = data.replace(/global\./g, 'window.')
		data = data.replace(/(const|let|var)\s+(\w+)\s*=\s*require\(\s*['|"](.+)['|"]\s*\)/g, importReplacer)
		data = data.replace(/module\.exports\s*=\s*/g, 'export default ')
	}
	fs.writeFileSync(output, data)
}

converter(path.resolve(__dirname, '..', 'cache.js'), path.resolve(__dirname, '.', 'cache.js'))

if(!fs.existsSync(path.resolve(__dirname, 'provider'))) fs.mkdirSync(path.resolve(__dirname, 'provider'))

fs.readdirSync(path.resolve(__dirname, '..', 'provider')).filter(file => !file.includes('test')).forEach(file => {
	converter(path.resolve(__dirname, '..', 'provider', file), path.resolve(__dirname, 'provider', file))
})

const providerReplacer = (match, state, data) => {
	let provider = []
	let imports = data.match(/\w+\s*:\s*require\(['|"].+['|"]\)/g).map(line => {
		line = line.match(/(\w+)\s*:\s*require\(['|"](.+)['|"]\)/)
		provider.push(line[1])
		return importReplacer(null, null, line[1], line[2])
	})
	return imports.join('\n') + '\n\n' + `${state} provider = {${provider.join(', ')}}`
}

converter(path.resolve(__dirname, 'provider', 'match.js'), path.resolve(__dirname, 'provider', 'match.js'), data => {
	data = data.replace(/(const|let|var)\s+provider\s*=\s*{([^}]+)}/g, providerReplacer)
	return data
})

================================================
FILE: src/browser/crypto.js
================================================

const bodyify = object => Object.entries(object).map(entry => entry.map(encodeURIComponent).join('=')).join('&')

const toBuffer = string => (new TextEncoder()).encode(string)
const toHex = arrayBuffer => Array.from(arrayBuffer).map(n => n.toString(16).padStart(2, '0')).join('')
const toBase64 = arrayBuffer => btoa(arrayBuffer)

export default {
	uri: {
		retrieve: id => {
			id = id.toString().trim()
			const key = '3go8&$8*3*3h0k(2)2'
			let string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('')
			let result = CryptoJS.MD5(string).toString(CryptoJS.enc.Base64).replace(/\//g, '_').replace(/\+/g, '-')
			return `http://p1.music.126.net/${result}/${id}`
		}
	},
	md5: {
		digest: value => CryptoJS.MD5(value).toString()
	},
	miguapi: {
		encrypt: object => {
			let text = JSON.stringify(object), signer = new JSEncrypt()
			let password = Array.from(window.crypto.getRandomValues(new Uint8Array(32))).map(n => n.toString(16).padStart(2, '0')).join('')
			signer.setPublicKey('-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----')
			return bodyify({
				data: CryptoJS.AES.encrypt(text, password).toString(),
				secKey: signer.encrypt(password)
			})
		}
	}
}

================================================
FILE: src/browser/inject.js
================================================
(() => {
	const remote = 'oleomikdicccalekkpcbfgdmpjehnpkp'
	const remoteMatch = id => new Promise(resolve => {
		chrome.runtime.sendMessage(remote, {match: id}, response => {
			resolve(response)
		})
	})

	const waitTimeout = wait => new Promise(resolve => {
		setTimeout(() => {
			resolve()
		}, wait)
	})

	const searchFunction = (object, keyword) =>
		Object.keys(object)
		.filter(key => object[key] && typeof object[key] == 'function')
		.find(key => String(object[key]).match(keyword))

	if(self.frameElement && self.frameElement.tagName == 'IFRAME'){ //in iframe
		const keyOne = searchFunction(window.nej.e, '\\.dataset;if')
		const keyTwo = searchFunction(window.nm.x, '\\.copyrightId==')
		const keyThree = searchFunction(window.nm.x, '\\.privilege;if')
		const functionOne = window.nej.e[keyOne]

		window.nej.e[keyOne] = (z, name) => {
			if (name == 'copyright' || name == 'resCopyright') return 1
			return functionOne(z, name)
		}
		window.nm.x[keyTwo] = () => false
		window.nm.x[keyThree] = song => {
			song.status = 0
			if (song.privilege) song.privilege.pl = 320000
			return 0
		}
		const table = document.querySelector('table tbody')
		if(table) Array.from(table.childNodes)
		.filter(element => element.classList.contains('js-dis'))
		.forEach(element => element.classList.remove('js-dis'))
	}
	else{
		const keyAjax = searchFunction(window.nej.j, '\\.replace\\("api","weapi')
		const functionAjax = window.nej.j[keyAjax]
		window.nej.j[keyAjax] = (url, param) => {
			const onload = param.onload
			param.onload = data => {
				Promise.resolve()
				.then(() => {
					if(url.includes('enhance/player/url')){
						if(data.data[0].url){
							data.data[0].url = data.data[0].url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net')
						}
						else{
							return Promise.race([remoteMatch(data.data[0].id), waitTimeout(4000)])
							.then(result => {
								if(result){
									data.data[0].code = 200
									data.data[0].br = 320000
									data.data[0].type = 'mp3'
									data.data[0].size = result.size
									data.data[0].md5 = result.md5
									data.data[0].url = result.url.replace(/http:\/\//, 'https://')
								}
							})
						}
					}
				})
				.then(() => onload(data))
			}
			functionAjax(url, param)
		}
	}
})()

================================================
FILE: src/browser/manifest.json
================================================
{
	"name": "UnblockNeteaseMusic",
	"description": "For test (es6 only)",
	"version": "0.1",
	"background": {
		"page": "background.html"
	},
	"content_scripts": [{
		"js": ["script.js"],
		"matches": ["*://music.163.com/*"],
		"all_frames": true
	}],
	"web_accessible_resources": ["inject.js"],
	"externally_connectable": {
		"matches": ["*://music.163.com/*"]
	},
	"manifest_version": 2,
	"permissions": ["*://*/*", "webRequest", "webRequestBlocking"],
	"content_security_policy": "script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net; object-src 'self'"
}

================================================
FILE: src/browser/request.js
================================================
export default (method, url, headers, body) => new Promise((resolve, reject) => {
	headers = headers || {}
	const xhr = new XMLHttpRequest()
	xhr.onreadystatechange = () => {if (xhr.readyState == 4) resolve(xhr)}
	xhr.onerror = error => reject(error)
	xhr.open(method, url, true)
	const safe = {}, unsafe = {}
	Object.keys(headers).filter(key => (['origin', 'referer'].includes(key.toLowerCase()) ? unsafe : safe)[key] = headers[key])
	Object.entries(safe).forEach(entry => xhr.setRequestHeader.apply(xhr, entry))
	if (Object.keys(unsafe)) xhr.setRequestHeader('Additional-Headers', btoa(JSON.stringify(unsafe)))
	xhr.send(body)
}).then(xhr => Object.assign(xhr, {
	statusCode: xhr.status,
	headers: 
		xhr.getAllResponseHeaders().split('\r\n').filter(line => line).map(line => line.split(/\s*:\s*/))
		.reduce((result, pair) => Object.assign(result, {[pair[0].toLowerCase()]: pair[1]}), {}),
	url: {href: xhr.responseURL},
	body: () => xhr.responseText,
	json: () => JSON.parse(xhr.responseText),
	jsonp: () => JSON.parse(xhr.responseText.slice(xhr.responseText.indexOf('(') + 1, -')'.length))
}))

================================================
FILE: src/browser/script.js
================================================
(() => {
	let script = (document.head || document.documentElement).appendChild(document.createElement('script'))
	script.src = chrome.extension.getURL('inject.js')
	script.onload = script.parentNode.removeChild(script)
})()

================================================
FILE: src/cache.js
================================================
const collector = (job, cycle) =>
	setTimeout(() => {
		let keep = false
		Object.keys(job.cache || {})
		.forEach(key => {
			if (!job.cache[key]) return
			job.cache[key].expiration < Date.now()
				? job.cache[key] = null
				: keep = keep || true
		})
		keep ? collector(job, cycle) : job.collector = null
	}, cycle)

module.exports = (job, parameter, live = 30 * 60 * 1000) => {
	const cache = job.cache ? job.cache : job.cache = {}
	if (!job.collector) job.collector = collector(job, live / 2)
	const key = parameter == null ? 'default' : (typeof(parameter) === 'object' ? (parameter.id || parameter.key || JSON.stringify(parameter)) : parameter)
	const done = (status, result) => cache[key].execution = Promise[status](result)
	if (!cache[key] || cache[key].expiration < Date.now())
		cache[key] = {
			expiration: Date.now() + live,
			execution: job(parameter)
				.then(result => done('resolve', result))
				.catch(result => done('reject', result))
		}
	return cache[key].execution
}

================================================
FILE: src/cli.js
================================================
const cli = {
	width: 80,
	_program: {},
	_options: [],
	program: (information = {}) => {
		cli._program = information
		return cli
	},
	option: (flags, addition = {}) => {
		// name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.
		// dest - The name of the attribute to be added to the object returned by parse_options().

		// nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER
		// 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

		// const - A constant value required by some action and nargs selections. (supporting store_const and append_const action)

		// metavar - A name for the argument in usage messages.
		// help - A brief description of what the argument does.

		// required - Whether or not the command-line option may be omitted (optionals only).
		// default - The value produced if the argument is absent from the command line.
		// type - The type to which the command-line argument should be converted.
		// choices - A container of the allowable values for the argument.

		flags = Array.isArray(flags) ? flags : [flags]
		addition.dest = addition.dest || flags.slice(-1)[0].toLowerCase().replace(/^-+/, '').replace(/-[a-z]/g, character => character.slice(1).toUpperCase())
		addition.help = addition.help || {'help': 'output usage information', 'version': 'output the version number'}[addition.action]
		cli._options.push(Object.assign(addition, {flags: flags, positional: !flags[0].startsWith('-')}))
		return cli
	},
	parse: argv => {
		const positionals = cli._options.map((option, index) => option.positional ? index : null).filter(index => index !== null), optionals = {}
		cli._options.forEach((option, index) => option.positional ? null : option.flags.forEach(flag => optionals[flag] = index))

		cli._program.name = cli._program.name || require('path').parse(argv[1]).base
		const args = argv.slice(2).reduce((result, part) => /^-[^-]/.test(part) ? result.concat(part.slice(1).split('').map(string => '-' + string)) : result.concat(part), [])

		let pointer = 0
		while (pointer < args.length) {
			let value = null
			const part = args[pointer]
			const index = part.startsWith('-') ? optionals[part] : positionals.shift()
			if (index == undefined) part.startsWith('-') ? error(`no such option: ${part}`) : error(`extra arguments found: ${part}`)
			if (part.startsWith('-')) pointer += 1
			const {action} = cli._options[index]

			if (['help', 'version'].includes(action)) {
				if (action === 'help') help()
				else if (action === 'version') version()
			}
			else if (['store_true', 'store_false'].includes(action)) {
				value = action === 'store_true'
			}
			else {
				const gap = args.slice(pointer).findIndex(part => part in optionals)
				const next = gap === -1 ? args.length : pointer + gap
				value = args.slice(pointer, next)
				if (value.length === 0) {
					if (cli._options[index].positional)
						error(`the following arguments are required: ${part}`)
					else if (cli._options[index].nargs === '+')
						error(`argument ${part}: expected at least one argument`)
					else
						error(`argument ${part}: expected one argument`)
				}
				if (cli._options[index].nargs !== '+') {
					value = value[0]
					pointer += 1
				}
				else {
					pointer = next
				}
			}
			cli[cli._options[index].dest] = value
		}
		if (positionals.length) error(`the following arguments are required: ${positionals.map(index => cli._options[index].flags[0]).join(', ')}`)
		// cli._options.forEach(option => console.log(option.dest, cli[option.dest]))
		return cli
	}
}

const pad = length => (new Array(length + 1)).join(' ')

const usage = () => {
	const options = cli._options.map(option => {
		const flag = option.flags.sort((a, b) => a.length - b.length)[0]
		const name = option.metavar || option.dest
		if (option.positional) {
			if (option.nargs === '+')
				return `${name} [${name} ...]`
			else
				return `${name}`
		}
		else {
			if (['store_true', 'store_false', 'help', 'version'].includes(option.action))
				return `[${flag}]`
			else if (option.nargs === '+')
				return `[${flag} ${name} [${name} ...]]`
			else
				return `[${flag} ${name}]`
		}
	})
	const maximum = cli.width
	const title = `usage: ${cli._program.name}`
	const lines = [title]

	options.map(name => ' ' + name).forEach(option => {
		lines[lines.length - 1].length + option.length < maximum
			? lines[lines.length - 1] += option
			: lines.push(pad(title.length) + option)
	})
	console.log(lines.join('\n'))
}

const help = () => {
	usage()
	const positionals = cli._options.filter(option => option.positional)
	.map(option => [option.metavar || option.dest, option.help])
	const optionals = cli._options.filter(option => !option.positional)
	.map(option => {
		const {flags} = option
		const name = option.metavar || option.dest
		let use = ''
		if (['store_true', 'store_false', 'help', 'version'].includes(option.action))
			use = flags.map(flag => `${flag}`).join(', ')
		else if (option.nargs === '+')
			use = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ')
		else
			use = flags.map(flag => `${flag} ${name}`).join(', ')
		return [use, option.help]
	})
	let align = Math.max.apply(null, positionals.concat(optionals).map(option => option[0].length))
	align = align > 30 ? 30 : align
	const rest = cli.width - align - 4
	const publish = option => {
		const slice = string =>
			Array.from(Array(Math.ceil(string.length / rest)).keys())
			.map(index => string.slice(index * rest, (index + 1) * rest))
			.join('\n' + pad(align + 4))
		option[0].length < align
			? console.log(`  ${option[0]}${pad(align - option[0].length)}  ${slice(option[1])}`)
			: console.log(`  ${option[0]}\n${pad(align + 4)}${slice(option[1])}`)
	}
	if (positionals.length) console.log('\npositional arguments:')
	positionals.forEach(publish)
	if (optionals.length) console.log('\noptional arguments:')
	optionals.forEach(publish)
	process.exit()
}

const version = () => {
	console.log(cli._program.version)
	process.exit()
}

const error = message => {
	usage()
	console.log(cli._program.name + ':', 'error:', message)
	process.exit(1)
}

module.exports = cli

================================================
FILE: src/crypto.js
================================================
'use strict'

const crypto = require('crypto')
const parse = require('url').parse
const bodyify = require('querystring').stringify

const eapiKey = 'e82ckenh8dichen8'
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'

const decrypt = (buffer, key) => {
	const decipher = crypto.createDecipheriv('aes-128-ecb', key, '')
	return Buffer.concat([decipher.update(buffer), decipher.final()])
}

const encrypt = (buffer, key) => {
	const cipher = crypto.createCipheriv('aes-128-ecb', key, '')
	return Buffer.concat([cipher.update(buffer), cipher.final()])
}

module.exports = {
	eapi: {
		encrypt: buffer => encrypt(buffer, eapiKey),
		decrypt: buffer => decrypt(buffer, eapiKey),
		encryptRequest: (url, object) => {
			url = parse(url)
			const text = JSON.stringify(object)
			const message = `nobody${url.path}use${text}md5forencrypt`
			const digest = crypto.createHash('md5').update(message).digest('hex')
			const data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
			return {
				url: url.href.replace(/\w*api/, 'eapi'),
				body: bodyify({
					params: module.exports.eapi.encrypt(Buffer.from(data)).toString('hex').toUpperCase()
				})
			}
		}
	},
	linuxapi: {
		encrypt: buffer => encrypt(buffer, linuxapiKey),
		decrypt: buffer => decrypt(buffer, linuxapiKey),
		encryptRequest: (url, object) => {
			url = parse(url)
			const text = JSON.stringify({method: 'POST', url: url.href, params: object})
			return {
				url: url.resolve('/api/linux/forward'),
				body: bodyify({
					eparams: module.exports.linuxapi.encrypt(Buffer.from(text)).toString('hex').toUpperCase()
				})
			}
		}
	},
	miguapi: {
		encryptBody: object => {
			const text = JSON.stringify(object)
			const derive = (password, salt, keyLength, ivSize) => { // EVP_BytesToKey
				salt = salt || Buffer.alloc(0)
				const keySize = keyLength / 8
				const repeat = Math.ceil((keySize + ivSize * 8) / 32)
				const buffer = Buffer.concat(Array(repeat).fill(null).reduce(
					result => result.concat(crypto.createHash('md5').update(Buffer.concat([result.slice(-1)[0], password, salt])).digest()),
					[Buffer.alloc(0)]
				))
				return {
					key: buffer.slice(0, keySize),
					iv: buffer.slice(keySize, keySize + ivSize)
				}
			}
			const password = Buffer.from(crypto.randomBytes(32).toString('hex')), salt = crypto.randomBytes(8)
			const key = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----'
			const secret = derive(password, salt, 256, 16)
			const cipher = crypto.createCipheriv('aes-256-cbc', secret.key, secret.iv)
			return bodyify({
				data: Buffer.concat([Buffer.from('Salted__'), salt, cipher.update(Buffer.from(text)), cipher.final()]).toString('base64'),
				secKey: crypto.publicEncrypt({key, padding: crypto.constants.RSA_PKCS1_PADDING}, password).toString('base64')
			})
		}
	},
	base64: {
		encode: (text, charset) => Buffer.from(text, charset).toString('base64').replace(/\+/g, '-').replace(/\//g, '_'),
		decode: (text, charset) => Buffer.from(text.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(charset)
	},
	uri: {
		retrieve: id => {
			id = id.toString().trim()
			const key = '3go8&$8*3*3h0k(2)2'
			const string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('')
			const result = crypto.createHash('md5').update(string).digest('base64').replace(/\//g, '_').replace(/\+/g, '-')
			return `http://p1.music.126.net/${result}/${id}`
		}
	},
	md5: {
		digest: value => crypto.createHash('md5').update(value).digest('hex'),
		pipe: source => new Promise((resolve, reject) => {
			const digest = crypto.createHash('md5').setEncoding('hex')
			source.pipe(digest)
			.on('error', error => reject(error))
			.once('finish', () => resolve(digest.read()))
		})
	}
}

try {module.exports.kuwoapi = require('./kwDES')} catch(e) {}

================================================
FILE: src/hook.js
================================================
const cache = require('./cache')
const parse = require('url').parse
const crypto = require('./crypto')
const request = require('./request')
const match = require('./provider/match')
const querystring = require('querystring')

const hook = {
	request: {
		before: () => {},
		after: () => {},
	},
	connect: {
		before: () => {}
	},
	negotiate: {
		before: () => {}
	},
	target: {
		host: new Set(),
		path: new Set()
	}
}

hook.target.host = new Set([
	'music.163.com',
	'interface.music.163.com',
	'interface3.music.163.com',
	'apm.music.163.com',
	'apm3.music.163.com',
	// 'mam.netease.com',
	// 'api.iplay.163.com', // look living
	// 'ac.dun.163yun.com',
	// 'crash.163.com',
	// 'clientlog.music.163.com',
	// 'clientlog3.music.163.com'
])

hook.target.path = new Set([
	'/api/v3/playlist/detail',
	'/api/v3/song/detail',
	'/api/v6/playlist/detail',
	'/api/album/play',
	'/api/artist/privilege',
	'/api/album/privilege',
	'/api/v1/artist',
	'/api/v1/artist/songs',
	'/api/artist/top/song',
	'/api/v1/album',
	'/api/album/v3/detail',
	'/api/playlist/privilege',
	'/api/song/enhance/player/url',
	'/api/song/enhance/player/url/v1',
	'/api/song/enhance/download/url',
	'/api/song/enhance/privilege',
	'/batch',
	'/api/batch',
	'/api/v1/search/get',
	'/api/v1/search/song/get',
	'/api/search/complex/get',
	'/api/cloudsearch/pc',
	'/api/v1/playlist/manipulate/tracks',
	'/api/song/like',
	'/api/v1/play/record',
	'/api/playlist/v4/detail',
	'/api/v1/radio/get',
	'/api/v1/discovery/recommend/songs'
])

const domainList = [
	'music.163.com', 
	'music.126.net',
	'iplay.163.com',
	'look.163.com',
	'y.163.com',
]

hook.request.before = ctx => {
	const {req} = ctx
	req.url = (req.url.startsWith('http://') ? '' : (req.socket.encrypted ? 'https:' : 'http:') + '//' + (domainList.some(domain => (req.headers.host || '').endsWith(domain)) ? req.headers.host : null)) + req.url
	const url = parse(req.url)
	if ([url.hostname, req.headers.host].some(host => host.includes('music.163.com'))) ctx.decision = 'proxy'
	if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host)) && req.method == 'POST' && (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/'))) {
		return request.read(req)
		.then(body => req.body = body)
		.then(body => {
			if ('x-napm-retry' in req.headers) delete req.headers['x-napm-retry']
			req.headers['X-Real-IP'] = '118.88.88.88'
			if (req.url.includes('stream')) return // look living eapi can not be decrypted
			if (body) {
				let data = null
				const netease = {}
				netease.pad = (body.match(/%0+$/) || [''])[0]
				netease.forward = (url.path == '/api/linux/forward')
				if (netease.forward) {
					data = JSON.parse(crypto.linuxapi.decrypt(Buffer.from(body.slice(8, body.length - netease.pad.length), 'hex')).toString())
					netease.path = parse(data.url).path
					netease.param = data.params
				}
				else {
					data = crypto.eapi.decrypt(Buffer.from(body.slice(7, body.length - netease.pad.length), 'hex')).toString().split('-36cd479b6b5-')
					netease.path = data[0]
					netease.param = JSON.parse(data[1])
				}
				netease.path = netease.path.replace(/\/\d*$/, '')
				ctx.netease = netease
				// console.log(netease.path, netease.param)

				if (netease.path == '/api/song/enhance/download/url')
					return pretendPlay(ctx)
			}
		})
		.catch(error => console.log(error, req.url))
	}
	else if ((hook.target.host.has(url.hostname)) && (url.path.startsWith('/weapi/') || url.path.startsWith('/api/'))) {
		req.headers['X-Real-IP'] = '118.88.88.88'
		ctx.netease = {web: true, path: url.path.replace(/^\/weapi\//, '/api/').replace(/\?.+$/, '').replace(/\/\d*$/, '')}
	}
	else if (req.url.includes('package')) {
		try {
			const data = req.url.split('package/').pop().split('/')
			const url = parse(crypto.base64.decode(data[0]))
			const id = data[1].replace(/\.\w+/, '')
			req.url = url.href
			req.headers['host'] = url.hostname
			req.headers['cookie'] = null
			ctx.package = {id}
			ctx.decision = 'proxy'
			// if (url.href.includes('google'))
			// 	return request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080'))
			// 	.then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res)))
		}
		catch(error) {
			ctx.error = error
			ctx.decision = 'close'
		}
	}
}

hook.request.after = ctx => {
	const {req, proxyRes, netease, package} = ctx
	if (req.headers.host === 'tyst.migu.cn' && proxyRes.headers['content-range'] && proxyRes.statusCode === 200) proxyRes.statusCode = 206
	if (netease && hook.target.path.has(netease.path) && proxyRes.statusCode == 200) {
		return request.read(proxyRes, true)
		.then(buffer => buffer.length ? proxyRes.body = buffer : Promise.reject())
		.then(buffer => {
			const patch = string => string.replace(/([^\\]"\s*:\s*)(\d{16,})(\s*[}|,])/g, '$1"$2L"$3') // for js precision
			try {
				netease.encrypted = false
				netease.jsonBody = JSON.parse(patch(buffer.toString()))
			}
			catch(error) {
				netease.encrypted = true
				netease.jsonBody = JSON.parse(patch(crypto.eapi.decrypt(buffer).toString()))
			}

			if (new Set([401, 512]).has(netease.jsonBody.code) && !netease.web) {
				if (netease.path.includes('manipulate')) return tryCollect(ctx)
				else if (netease.path == '/api/song/like') return tryLike(ctx)
			}
			else if (netease.path.includes('url')) return tryMatch(ctx)
		})
		.then(() => {
			['transfer-encoding', 'content-encoding', 'content-length'].filter(key => key in proxyRes.headers).forEach(key => delete proxyRes.headers[key])

			const inject = (key, value) => {
				if (typeof(value) === 'object' && value != null) {
					if ('fee' in value) value['fee'] = 0
					if ('st' in value && 'pl' in value && 'dl' in value && 'subp' in value) { // batch modify
						value['st'] = 0
						value['subp'] = 1
						value['pl'] = (value['pl'] == 0) ? 320000 : value['pl']
						value['dl'] = (value['dl'] == 0) ? 320000 : value['dl']
					}
				}
				return value
			}

			let body = JSON.stringify(netease.jsonBody, inject)
			body = body.replace(/([^\\]"\s*:\s*)"(\d{16,})L"(\s*[}|,])/g, '$1$2$3') // for js precision
			proxyRes.body = (netease.encrypted ? crypto.eapi.encrypt(Buffer.from(body)) : body)
		})
		.catch(error => error ? console.log(error, req.url) : null)
	}
	else if (package) {
		if (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) {
			return request(req.method, parse(req.url).resolve(proxyRes.headers.location), req.headers)
			.then(response => ctx.proxyRes = response)
		}
		else if (/p\d+c*.music.126.net/.test(req.url)) {
			proxyRes.headers['content-type'] = 'audio/*'
		}
	}
}

hook.connect.before = ctx => {
	const {req} = ctx
	const url = parse('https://' + req.url)
	if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host))) {
		if (url.port == 80) {
			req.url = `${global.address || 'localhost'}:${global.port[0]}`
			req.local = true
		}
		else if (global.port[1]) {
			req.url = `${global.address || 'localhost'}:${global.port[1]}`
			req.local = true
		}
		else {
			ctx.decision = 'blank'
		}
	}
	else if (url.href.includes(global.endpoint)) ctx.decision = 'proxy'
}

hook.negotiate.before = ctx => {
	const {req, socket, decision} = ctx
	const url = parse('https://' + req.url)
	const target = hook.target.host
	if (req.local || decision) return
	if (target.has(socket.sni) && !target.has(url.hostname)) {
		target.add(url.hostname)
		ctx.decision = 'blank'
	}
}

const pretendPlay = ctx => {
	const {req, netease} = ctx
	const turn = 'http://music.163.com/api/song/enhance/player/url'
	let query = null
	if (netease.forward) {
		const {id, br} = netease.param
		netease.param = {ids: `["${id}"]`, br}
		query = crypto.linuxapi.encryptRequest(turn, netease.param)
	}
	else {
		const {id, br, e_r, header} = netease.param
		netease.param = {ids: `["${id}"]`, br, e_r, header}
		query = crypto.eapi.encryptRequest(turn, netease.param)
	}
	req.url = query.url
	req.body = query.body + netease.pad
}

const tryCollect = ctx => {
	const {req, netease} = ctx
	const {trackIds, pid, op} = netease.param
	const trackId = (Array.isArray(trackIds) ? trackIds : JSON.parse(trackIds))[0]
	return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=${op}`).then(response => response.json())
	.then(jsonBody => {
		netease.jsonBody = jsonBody
	})
	.catch(() => {})
}

const tryLike = ctx => {
	const {req, netease} = ctx
	const {trackId} = netease.param
	let pid = 0, userId = 0
	return request('GET', 'http://music.163.com/api/v1/user/info', req.headers).then(response => response.json())
	.then(jsonBody => {
		userId = jsonBody.userPoint.userId
		return request('GET', `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, req.headers).then(response => response.json())
	})
	.then(jsonBody => {
		pid = jsonBody.playlist[0].id
		return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add`).then(response => response.json())
	})
	.then(jsonBody => {
		if (new Set([200, 502]).has(jsonBody.code)) {
			netease.jsonBody = {code: 200, playlistId: pid}
		}
	})
	.catch(() => {})
}

const computeHash = task => request('GET', task.url).then(response => crypto.md5.pipe(response))

const tryMatch = ctx => {
	const {req, netease} = ctx
	const {jsonBody} = netease
	let tasks = [], target = 0

	const inject = item => {
		item.flag = 0
		if ((item.code != 200 || item.freeTrialInfo) && (target == 0 || item.id == target)) {
			return match(item.id)
			.then(song => {
				item.type = song.br === 999000 ? 'flac' : 'mp3'
				item.url = global.endpoint ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${item.id}.${item.type}` : song.url
				item.md5 = song.md5 || crypto.md5.digest(song.url)
				item.br = song.br || 128000
				item.size = song.size
				item.code = 200
				item.freeTrialInfo = null
				return song
			})
			.then(song => {
				if (!netease.path.includes('download') || song.md5) return
				const newer = (base, target) => {
					const difference =
						Array.from([base, target])
						.map(version => version.split('.').slice(0, 3).map(number => parseInt(number) || 0))
						.reduce((aggregation, current) => !aggregation.length ? current.map(element => [element]) : aggregation.map((element, index) => element.concat(current[index])), [])
						.filter(pair => pair[0] != pair[1])[0]
					return !difference || difference[0] <= difference[1]
				}
				const limit = {android: '0.0.0', osx: '0.0.0'}
				const task = {key: song.url.replace(/\?.*$/, '').replace(/(?<=kugou\.com\/)\w+\/\w+\//, '').replace(/(?<=kuwo\.cn\/)\w+\/\w+\/resource\//, ''), url: song.url}
				try {
					let {header} = netease.param
					header = typeof(header) === 'string' ? JSON.parse(header) : header
					const cookie = querystring.parse(req.headers.cookie.replace(/\s/g, ''), ';')
					const os = header.os || cookie.os, version = header.appver || cookie.appver
					if (os in limit && newer(limit[os], version))
						return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value)
				}
				catch(e) {}
			})
			.catch(() => {})
		}
		else if (item.code == 200 && netease.web) {
			item.url = item.url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net')
		}
	}

	if (!Array.isArray(jsonBody.data)) {
		tasks = [inject(jsonBody.data)]
	}
	else if (netease.path.includes('download')) {
		jsonBody.data = jsonBody.data[0]
		tasks = [inject(jsonBody.data)]
	}
	else {
		target = 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
		tasks = jsonBody.data.map(item => inject(item))
	}
	return Promise.all(tasks).catch(() => {})
}

module.exports = hook

================================================
FILE: src/kwDES.js
================================================
/*
	Thanks to
	https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
	https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
*/

const Long = (
	typeof(BigInt) === 'function' // BigInt support in Node 10+
		? n => (n = BigInt(n), ({
			low: Number(n),
			valueOf: () => n.valueOf(),
			toString: () => n.toString(),
			not: () => Long(~n),
			isNegative: () => n < 0,
			or: x => Long(n | BigInt(x)),
			and: x => Long(n & BigInt(x)),
			xor: x => Long(n ^ BigInt(x)),
			equals: x => n === BigInt(x),
			multiply: x => Long(n * BigInt(x)),
			shiftLeft: x => Long(n << BigInt(x)),
			shiftRight: x => Long(n >> BigInt(x)),
		}))
		: (...args) => new (require('long'))(...args)
)

const range = n => Array.from(new Array(n).keys())
const power = (base, index) => Array(index).fill().reduce((result) => result.multiply(base), Long(1))
const LongArray = (...array) => array.map(n => n === -1 ? Long(-1, -1) : Long(n))

// EXPANSION
const arrayE = LongArray(
	31,  0,  1,  2,  3,  4, -1, -1,
	 3,  4,  5,  6,  7,  8, -1, -1,
	 7,  8,  9, 10, 11, 12, -1, -1,
	11, 12, 13, 14, 15, 16, -1, -1,
	15, 16, 17, 18, 19, 20, -1, -1,
	19, 20, 21, 22, 23, 24, -1, -1,
	23, 24, 25, 26, 27, 28, -1, -1,
	27, 28, 29, 30, 31, 30, -1, -1
)

// INITIAL_PERMUTATION
const arrayIP = LongArray(
	57, 49, 41, 33, 25, 17,  9,  1,
	59, 51, 43, 35, 27, 19, 11,  3,
	61, 53, 45, 37, 29, 21, 13,  5,
	63, 55, 47, 39, 31, 23, 15,  7,
	56, 48, 40, 32, 24, 16,  8,  0,
	58, 50, 42, 34, 26, 18, 10,  2,
	60, 52, 44, 36, 28, 20, 12,  4,
	62, 54, 46, 38, 30, 22, 14,  6
)

// INVERSE_PERMUTATION
const arrayIP_1 = LongArray(
	39,  7, 47, 15, 55, 23, 63, 31,
	38,  6, 46, 14, 54, 22, 62, 30,
	37,  5, 45, 13, 53, 21, 61, 29,
	36,  4, 44, 12, 52, 20, 60, 28,
	35,  3, 43, 11, 51, 19, 59, 27,
	34,  2, 42, 10, 50, 18, 58, 26,
	33,  1, 41,  9, 49, 17, 57, 25,
	32,  0, 40,  8, 48, 16, 56, 24
)

// ROTATES
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
const arrayLsMask = LongArray(0, 0x100001, 0x300003)
const arrayMask = range(64).map(n => power(2, n))
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1)

// PERMUTATION
const arrayP = LongArray(
	15,  6, 19, 20, 28, 11, 27, 16,
	 0, 14, 22, 25,  4, 17, 30,  9,
	 1,  7, 23, 13, 31, 26,  2,  8,
	18, 12, 29,  5, 21, 10,  3, 24
)

// PERMUTED_CHOICE1
const arrayPC_1 = LongArray(
	56, 48, 40, 32, 24, 16,  8,  0,
	57, 49, 41, 33, 25, 17,  9,  1,
	58, 50, 42, 34, 26, 18, 10,  2,
	59, 51, 43, 35, 62, 54, 46, 38,
	30, 22, 14,  6, 61, 53, 45, 37,
	29, 21, 13,  5, 60, 52, 44, 36,
	28, 20, 12,  4, 27, 19, 11,  3
)

// PERMUTED_CHOICE2
const arrayPC_2 = LongArray(
	13, 16, 10, 23,  0,  4, -1, -1,
	 2, 27, 14,  5, 20,  9, -1, -1,
	22, 18, 11,  3, 25,  7, -1, -1,
	15,  6, 26, 19, 12,  1, -1, -1,
	40, 51, 30, 36, 46, 54, -1, -1,
	29, 39, 50, 44, 32, 47, -1, -1,
	43, 48, 38, 55, 33, 52, -1, -1,
	45, 41, 49, 35, 28, 31, -1, -1
)

const matrixNSBox = [[
	14,  4,  3, 15,  2, 13,  5,  3,
	13, 14,  6,  9, 11,  2,  0,  5,
	 4,  1, 10, 12, 15,  6,  9, 10,
	 1,  8, 12,  7,  8, 11,  7,  0,
	 0, 15, 10,  5, 14,  4,  9, 10,
	 7,  8, 12,  3, 13,  1,  3,  6,
	15, 12,  6, 11,  2,  9,  5,  0,
	 4,  2, 11, 14,  1,  7,  8, 13, ], [
	15,  0,  9,  5,  6, 10, 12,  9,
	 8,  7,  2, 12,  3, 13,  5,  2,
	 1, 14,  7,  8, 11,  4,  0,  3,
	14, 11, 13,  6,  4,  1, 10, 15,
	 3, 13, 12, 11, 15,  3,  6,  0,
	 4, 10,  1,  7,  8,  4, 11, 14,
	13,  8,  0,  6,  2, 15,  9,  5,
	 7,  1, 10, 12, 14,  2,  5,  9, ], [
	10, 13,  1, 11,  6,  8, 11,  5,
	 9,  4, 12,  2, 15,  3,  2, 14,
	 0,  6, 13,  1,  3, 15,  4, 10,
	14,  9,  7, 12,  5,  0,  8,  7,
	13,  1,  2,  4,  3,  6, 12, 11,
	 0, 13,  5, 14,  6,  8, 15,  2,
	 7, 10,  8, 15,  4,  9, 11,  5,
	 9,  0, 14,  3, 10,  7,  1, 12, ], [
	 7, 10,  1, 15,  0, 12, 11,  5,
	14,  9,  8,  3,  9,  7,  4,  8,
	13,  6,  2,  1,  6, 11, 12,  2,
	 3,  0,  5, 14, 10, 13, 15,  4,
	13,  3,  4,  9,  6, 10,  1, 12,
	11,  0,  2,  5,  0, 13, 14,  2,
	 8, 15,  7,  4, 15,  1, 10,  7,
	 5,  6, 12, 11,  3,  8,  9, 14, ], [
	 2,  4,  8, 15,  7, 10, 13,  6,
	 4,  1,  3, 12, 11,  7, 14,  0,
	12,  2,  5,  9, 10, 13,  0,  3,
	 1, 11, 15,  5,  6,  8,  9, 14,
	14, 11,  5,  6,  4,  1,  3, 10,
	 2, 12, 15,  0, 13,  2,  8,  5,
	11,  8,  0, 15,  7, 14,  9,  4,
	12,  7, 10,  9,  1, 13,  6,  3, ], [
	12,  9,  0,  7,  9,  2, 14,  1,
	10, 15,  3,  4,  6, 12,  5, 11,
	 1, 14, 13,  0,  2,  8,  7, 13,
	15,  5,  4, 10,  8,  3, 11,  6,
	10,  4,  6, 11,  7,  9,  0,  6,
	 4,  2, 13,  1,  9, 15,  3,  8,
	15,  3,  1, 14, 12,  5, 11,  0,
	 2, 12, 14,  7,  5, 10,  8, 13, ], [
	 4,  1,  3, 10, 15, 12,  5,  0,
	 2, 11,  9,  6,  8,  7,  6,  9,
	11,  4, 12, 15,  0,  3, 10,  5,
	14, 13,  7,  8, 13, 14,  1,  2,
	13,  6, 14,  9,  4,  1,  2, 14,
	11, 13,  5,  0,  1, 10,  8,  3,
	 0, 11,  3,  5,  9,  4, 15,  2,
	 7,  8, 12, 15, 10,  7,  6, 12, ], [
	13,  7, 10,  0,  6,  9,  5, 15,
	 8,  4,  3, 10, 11, 14, 12,  5,
	 2, 11,  9,  6, 15, 12,  0,  3,
	 4,  1, 14, 13,  1,  2,  7,  8,
	 1,  2, 12, 15, 10,  4,  0,  3,
	13, 14,  6,  9,  7,  8,  9,  6,
	15,  1,  5, 12,  3, 10, 14,  5,
	 8,  7, 11,  0,  4, 13,  2, 11, ],
]

const bitTransform = (arrInt, n, l) => { // int[], int, long : long
	let l2 = Long(0)
	range(n).forEach(i => {
		if (arrInt[i].isNegative() || (l.and(arrayMask[arrInt[i].low]).equals(0)))
			return
		l2 = l2.or(arrayMask[i])
	})
	return l2
}

const DES64 = (longs, l) => { // long[], long
	let out = Long(0)
	let SOut = Long(0)
	const pR = range(8).map(() => Long(0))
	const pSource = [Long(0), Long(0)]
	let L = Long(0)
	let R = Long(0)
	out = bitTransform(arrayIP, 64, l)
	pSource[0] = out.and(0xFFFFFFFF)
	pSource[1] = out.and(-4294967296).shiftRight(32)

	range(16).forEach(i => {
		R = Long(pSource[1])
		R = bitTransform(arrayE, 64, R)
		R = R.xor(longs[i])
		range(8).forEach(j => {
			pR[j] = R.shiftRight(j * 8).and(255)
		})
		SOut = Long(0)
		range(8).reverse().forEach(sbi => {
			SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]])
		})
		R = bitTransform(arrayP, 32, SOut)
		L = Long(pSource[0])
		pSource[0] = Long(pSource[1])
		pSource[1] = L.xor(R)
	})
	pSource.reverse()
	out = pSource[1].shiftLeft(32).and(-4294967296).or(
		pSource[0].and(0xFFFFFFFF)
	)
	out = bitTransform(arrayIP_1, 64, out)
	return out
}


const subKeys = (l, longs, n) => { // long, long[], int
	let l2 = bitTransform(arrayPC_1, 56, l)
	range(16).forEach(i => {
		l2 = (
			l2.and(arrayLsMask[arrayLs[i]]).shiftLeft(28 - arrayLs[i]).or(
				l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i])
			)
		)
		longs[i] = bitTransform(arrayPC_2, 64, l2)
	})
	if (n === 1) {
		range(8).forEach(j => {
			[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]]
		})
	}
}

const crypt = (msg, key, mode) => {
	// 处理密钥块
	let l = Long(0)
	range(8).forEach(i => {
		l = Long(key[i]).shiftLeft(i * 8).or(l)
	})

	const j = Math.floor(msg.length / 8)
	// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
	
	const arrLong1 = range(16).map(() => Long(0))
	subKeys(l, arrLong1, mode)

	// arrLong2 存放的是前部分的明文
	const arrLong2 = range(j).map(() => Long(0))

	range(j).forEach(m => {
		range(8).forEach(n => {
			arrLong2[m] = Long(msg[n + m * 8]).shiftLeft(n * 8).or(arrLong2[m])
		})
	})

	// 用于存放密文
	const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0))

	// 计算前部的数据块(除了最后一部分)
	range(j).forEach(i1 => {
		arrLong3[i1] = DES64(arrLong1, arrLong2[i1])
	})

	// 保存多出来的字节
	const arrByte1 = msg.slice(j * 8)
	let l2 = Long(0)

	range(msg.length % 8).forEach(i1 => {
		l2 = Long(arrByte1[i1]).shiftLeft(i1 * 8).or(l2)
	})

	// 计算多出的那一位(最后一位)
	if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2) // 解密不需要

	// 将密文转为字节型
	const arrByte2 = range(8 * arrLong3.length).map(() => 0)
	let i4 = 0
	arrLong3.forEach(l3 => {
		range(8).forEach(i6 => {
			arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low
			i4 += 1
		})
	})
	return Buffer.from(arrByte2)
}

const SECRET_KEY = Buffer.from('ylzsxkwm')
const encrypt = msg => crypt(msg, SECRET_KEY, 0)
const decrypt = msg => crypt(msg, SECRET_KEY, 1)
const encryptQuery = query => encrypt(Buffer.from(query)).toString('base64')

module.exports = {encrypt, decrypt, encryptQuery}

================================================
FILE: src/provider/baidu.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const request = require('../request')

const format = song => {
	const artistId = song.all_artist_id.split(',')
	return {
		id: song.song_id,
		name: song.title,
		album: {id: song.album_id, name: song.album_title},
		artists: song.author.split(',').map((name, index) => ({id: artistId[index], name}))
	}
}

const search = info => {
	const url =
		'http://musicapi.taihe.com/v1/restserver/ting?' +
		'from=qianqianmini&method=baidu.ting.search.merge&' +
		'isNew=1&platform=darwin&page_no=1&page_size=30&' +
		`query=${encodeURIComponent(info.keyword)}&version=11.2.1`

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => {
		const list = jsonBody.result.song_info.song_list.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

const track = id => {
	const url =
		'http://music.taihe.com/data/music/fmlink?' +
		'songIds=' + id + '&type=mp3'

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => {
		if ('songList' in jsonBody.data)
			return jsonBody.data.songList[0].songLink || Promise.reject()
		else
			return Promise.reject()
	})
	.catch(() => insure().baidu.track(id))
}

const check = info => cache(search, info).then(track)

module.exports = {check}

================================================
FILE: src/provider/find.js
================================================
const cache = require('../cache')
const request = require('../request')

const filter = (object, keys) => Object.keys(object).reduce((result, key) => Object.assign(result, keys.includes(key) && {[key]: object[key]}), {})
// Object.keys(object).filter(key => !keys.includes(key)).forEach(key => delete object[key])

const limit = text => {
	const output = [text[0]]
	const length = () => output.reduce((sum, token) => sum + token.length, 0)
	text.slice(1).some(token => length() > 15 ? true : (output.push(token), false))
	return output
}

const find = id => {
	const url =
		'https://music.163.com/api/song/detail?ids=[' + id + ']'

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => {
		const info = filter(jsonBody.songs[0], ['id', 'name', 'alias', 'duration'])
		info.name = (info.name || '')
			.replace(/(\s*cover[::\s][^)]+)/i, '')
			.replace(/\(\s*cover[::\s][^\)]+\)/i, '')
			.replace(/(\s*翻自[::\s][^)]+)/, '')
			.replace(/\(\s*翻自[::\s][^\)]+\)/, '')
		info.album = filter(jsonBody.songs[0].album, ['id', 'name'])
		info.artists = jsonBody.songs[0].artists.map(artist => filter(artist, ['id', 'name']))
		info.keyword = info.name + ' - ' + limit(info.artists.map(artist => artist.name)).join(' / ')
		return info.name ? info : Promise.reject()
	})
}

module.exports = id => cache(find, id)

================================================
FILE: src/provider/insure.js
================================================
const request = require('../request')
const host = null // 'http://localhost:9000'

module.exports = () => {
	const proxy = new Proxy(() => {}, {
		get: (target, property) => {
			target.route = (target.route || []).concat(property)
			return proxy
		},
		apply: (target, _, payload) => {
			if (module.exports.disable || !host) return Promise.reject()
			const path = target.route.join('/')
			const query = typeof(payload[0]) === 'object' ? JSON.stringify(payload[0]) : payload[0]
			// if (path != 'qq/ticket') return Promise.reject()
			return request('GET', `${host}/${path}?${encodeURIComponent(query)}`)
			.then(response => response.body())
		}
	})
	return proxy
}

================================================
FILE: src/provider/joox.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const crypto = require('../crypto')
const request = require('../request')

const headers = {
	'origin': 'http://www.joox.com',
	'referer': 'http://www.joox.com'
}

const fit = info => {
	if (/[\u0800-\u4e00]/.test(info.name)) //is japanese
		return info.name
	else
		return info.keyword
}

const format = song => {
	const {decode} = crypto.base64
	return {
		id: song.songid,
		name: decode(song.info1 || ''),
		duration: song.playtime * 1000,
		album: {id: song.albummid, name: decode(song.info3 || '')},
		artists: song.singer_list.map(({id, name}) => ({id, name: decode(name || '')}))
	}
}

const search = info => {
	const keyword = fit(info)
	const url =
		'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' +
		'country=hk&lang=zh_TW&' +
		'search_input=' + encodeURIComponent(keyword) + '&sin=0&ein=30'

	return request('GET', url, headers)
	.then(response => response.body())
	.then(body => {
		const jsonBody = JSON.parse(body.replace(/'/g, '"'))
		const list = jsonBody.itemlist.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

const track = id => {
	const url =
		'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' +
		'songid=' + id + '&country=hk&lang=zh_cn&from_type=-1&' +
		'channel_id=-1&_=' + (new Date).getTime()

	return request('GET', url, headers)
	.then(response => response.jsonp())
	.then(jsonBody => {
		const songUrl = (jsonBody.r320Url || jsonBody.r192Url || jsonBody.mp3Url || jsonBody.m4aUrl).replace(/M\d00([\w]+).mp3/, 'M800$1.mp3')
		if (songUrl)
			return songUrl
		else
			return Promise.reject()
	})
	.catch(() => insure().joox.track(id))
}

const check = info => cache(search, info).then(track)

module.exports = {check, track}

================================================
FILE: src/provider/kugou.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const crypto = require('../crypto')
const request = require('../request')

const format = song => {
	const SingerName = song.SingerName.split('、')
	return {
		id: song.FileHash,
		name: song.SongName,
		duration: song.Duration * 1000,
		album: {id: song.AlbumID, name: song.AlbumName},
		artists: song.SingerId.map((id, index) => ({id, name: SingerName[index]}))
	}
}

const search = info => {
	const url =
		'http://songsearch.kugou.com/song_search_v2?' +
		'keyword=' + encodeURIComponent(info.keyword) + '&page=1'

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => {
		const list = jsonBody.data.lists.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
	.catch(() => insure().kugou.search(info))
}

const track = id => {
	// const url =
	// 	'http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=' + id

	// return request('GET', url)
	// .then(response => response.json())
	// .then(jsonBody => jsonBody.url || Promise.reject())

	const url =
		'http://trackercdn.kugou.com/i/v2/?' +
		'key=' + crypto.md5.digest(`${id}kgcloudv2`) + '&hash=' + id + '&' +
		'br=hq&appid=1005&pid=2&cmd=25&behavior=play'

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => jsonBody.url[0] || Promise.reject())
}

const check = info => cache(search, info).then(track)

module.exports = {check, search}

================================================
FILE: src/provider/kuwo.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const crypto = require('../crypto')
const request = require('../request')

const format = song => ({
	id: song.musicrid.split('_').pop(),
	name: song.name,
	duration: song.songTimeMinutes.split(':').reduce((minute, second) => minute * 60 + parseFloat(second), 0) * 1000,
	album: {id: song.albumid, name: song.album},
	artists: song.artist.split('&').map((name, index) => ({id: index ? null : song.artistid, name}))
})

const search = info => {
	// const url =
	// 	// 'http://search.kuwo.cn/r.s?' +
	// 	// 'ft=music&itemset=web_2013&client=kt&' +
	// 	// 'rformat=json&encoding=utf8&' +
	// 	// 'all=' + encodeURIComponent(info.keyword) + '&pn=0&rn=20'
	// 	'http://search.kuwo.cn/r.s?' +
	// 	'ft=music&rformat=json&encoding=utf8&' +
	// 	'rn=8&callback=song&vipver=MUSIC_8.0.3.1&' +
	// 	'SONGNAME=' + encodeURIComponent(info.name) + '&' +
	// 	'ARTIST=' + encodeURIComponent(info.artists[0].name)

	// return request('GET', url)
	// .then(response => response.body())
	// .then(body => {
	// 	const jsonBody = eval(
	// 		'(' + body
	// 		.replace(/\n/g, '')
	// 		.match(/try\s*\{[^=]+=\s*(.+?)\s*\}\s*catch/)[1]
	// 		.replace(/;\s*song\s*\(.+\)\s*;\s*/, '') + ')'
	// 	)
	// 	const matched = jsonBody.abslist[0]
	// 	if (matched)
	// 		return matched.MUSICRID.split('_').pop()
	// 	else
	// 		return Promise.reject()
	// })

	const keyword = encodeURIComponent(info.keyword.replace(' - ', ''))
	const url = `http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=${keyword}&pn=1&rn=30`

	return request('GET', `http://kuwo.cn/search/list?key=${keyword}`)
	.then(response => response.headers['set-cookie'].find(line => line.includes('kw_token')).replace(/;.*/, '').split('=').pop())
	.then(token => request('GET', url, {referer: `http://www.kuwo.cn/search/list?key=${keyword}`, csrf: token, cookie: `kw_token=${token}`}))
	.then(response => response.json())
	.then(jsonBody => {
		const list = jsonBody.data.list.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

const track = id => {
	const url = (crypto.kuwoapi
		? 'http://mobi.kuwo.cn/mobi.s?f=kuwo&q=' + crypto.kuwoapi.encryptQuery(
			'corp=kuwo&p2p=1&type=convert_url2&sig=0&format=' + ['flac', 'mp3'].slice(select.ENABLE_FLAC ? 0 : 1).join('|') + '&rid=' + id
		)
		: 'http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_' + id // flac refuse
		// : 'http://www.kuwo.cn/url?format=mp3&response=url&type=convert_url3&br=320kmp3&rid=' + id // flac refuse
	)

	return request('GET', url, {'user-agent': 'okhttp/3.10.0'})
	.then(response => response.body())
	.then(body => {
		const url = (body.match(/http[^\s$"]+/) || [])[0]
		return url || Promise.reject()
	})
	.catch(() => insure().kuwo.track(id))
}

const check = info => cache(search, info).then(track)

module.exports = {check, track}


================================================
FILE: src/provider/match.js
================================================
const find = require('./find')
const request = require('../request')

const provider = {
	netease: require('./netease'),
	qq: require('./qq'),
	xiami: require('./xiami'),
	baidu: require('./baidu'),
	kugou: require('./kugou'),
	kuwo: require('./kuwo'),
	migu: require('./migu'),
	joox: require('./joox'),
	youtube: require('./youtube')
}

const match = (id, source) => {
	let meta = {}
	const candidate = (source || global.source || ['qq', 'kuwo', 'migu']).filter(name => name in provider)
	return find(id)
	.then(info => {
		meta = info
		return Promise.all(candidate.map(name => provider[name].check(info).catch(() => {})))
	})
	.then(urls => {
		urls = urls.filter(url => url)
		return Promise.all(urls.map(url => check(url)))
	})
	.then(songs => {
		songs = songs.filter(song => song.url)
		if (!songs.length) return Promise.reject()
		console.log(`[${meta.id}] ${meta.name}\n${songs[0].url}`)
		return songs[0]
	})
}

const check = url => {
	const song = {size: 0, br: null, url: null, md5: null}
	return Promise.race([request('GET', url, {'range': 'bytes=0-8191'}), new Promise((_, reject) => setTimeout(() => reject(504), 5 * 1000))])
	.then(response => {
		if (!response.statusCode.toString().startsWith('2')) return Promise.reject()
		if (url.includes('qq.com'))
			song.md5 = response.headers['server-md5']
		else if (url.includes('xiami.net') || url.includes('qianqian.com'))
			song.md5 = response.headers['etag'].replace(/"/g, '').toLowerCase()
		song.size = parseInt((response.headers['content-range'] || '').split('/').pop() || response.headers['content-length']) || 0
		song.url = response.url.href
		return response.headers['content-length'] === '8192' ? response.body(true) : Promise.reject()
	})
	.then(data => {
		const bitrate = decode(data)
		song.br = (bitrate && !isNaN(bitrate)) ? bitrate * 1000 : null
	})
	.catch(() => {})
	.then(() => song)
}

const decode = buffer => {
	const map = {
		3: {
			3: ['free', 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 'bad'],
			2: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 'bad'],
			1: ['free', 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 'bad']
		},
		2: {
			3: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 'bad'],
			2: ['free', 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 'bad']
		}
	}
	map[2][1] = map[2][2]
	map[0] = map[2]

	let pointer = 0
	if (buffer.slice(0, 4).toString() === 'fLaC') return 999
	if (buffer.slice(0, 3).toString() === 'ID3') {
		pointer = 6
		const size = buffer.slice(pointer, pointer + 4).reduce((summation, value, index) => summation + (value & 0x7f) << (7 * (3 - index)), 0)
		pointer = 10 + size
	}
	const header = buffer.slice(pointer, pointer + 4)

	// https://www.allegro.cc/forums/thread/591512/674023
	if (
		header.length === 4 &&
		header[0] === 0xff &&
		((header[1] >> 5) & 0x7) === 0x7 &&
		((header[1] >> 1) & 0x3) !== 0 &&
		((header[2] >> 4) & 0xf) !== 0xf &&
		((header[2] >> 2) & 0x3) !== 0x3
	) {
		const version = (header[1] >> 3) & 0x3
		const layer = (header[1] >> 1) & 0x3
		const bitrate = header[2] >> 4
		return map[version][layer][bitrate]
	}
}

module.exports = match

================================================
FILE: src/provider/migu.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const crypto = require('../crypto')
const request = require('../request')

const headers = {
	'origin': 'http://music.migu.cn/',
	'referer': 'http://music.migu.cn/'
}

const format = song => {
	const singerId = song.singerId.split(/\s*,\s*/)
	const singerName = song.singerName.split(/\s*,\s*/)
	return {
		id: song.copyrightId,
		name: song.title,
		album: {id: song.albumId, name: song.albumName},
		artists: singerId.map((id, index) => ({id, name: singerName[index]}))
	}
}

const search = info => {
	const url =
		'http://m.music.migu.cn/migu/remoting/scr_search_tag?' +
		'keyword=' + encodeURIComponent(info.keyword) + '&type=2&rows=20&pgc=1'

	return request('GET', url)
	.then(response => response.json())
	.then(jsonBody => {
		const list = ((jsonBody || {}).musics || []).map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

const single = (id, format) => {
	const url =
		'http://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?' +
		'dataType=2&' + crypto.miguapi.encryptBody({copyrightId: id.toString(), type: format})

	return request('GET', url, headers)
	.then(response => response.json())
	.then(jsonBody => {
		const {playUrl} = jsonBody.data
		return playUrl ? encodeURI(playUrl) : Promise.reject()
	})
}

const track = id =>
	Promise.all(
		[3, 2, 1].slice(select.ENABLE_FLAC ? 0 : 1)
		.map(format => single(id, format).catch(() => null))
	)
	.then(result => result.find(url => url) || Promise.reject())
	.catch(() => insure().migu.track(id))

const check = info => cache(search, info).then(track)

module.exports = {check, track}

================================================
FILE: src/provider/netease.js
================================================
const cache = require('../cache')
const crypto = require('../crypto')
const request = require('../request')

const search = info => {
	const url =
		'http://music.163.com/api/album/' + info.album.id

	return request('GET', url)
	.then(response => response.body())
	.then(body => {
		const jsonBody = JSON.parse(body.replace(/"dfsId":(\d+)/g, '"dfsId":"$1"')) // for js precision
		const matched = jsonBody.album.songs.find(song => song.id === info.id)
		if (matched)
			return matched.hMusic.dfsId || matched.mMusic.dfsId || matched.lMusic.dfsId
		else
			return Promise.reject()
	})
}

const track = id => {
	if (!id || id === '0') return Promise.reject()
	return crypto.uri.retrieve(id)
}

const check = info => cache(search, info).then(track)

module.exports = {check}

================================================
FILE: src/provider/qq.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const request = require('../request')

const headers = {
	'origin': 'http://y.qq.com/',
	'referer': 'http://y.qq.com/',
	'cookie': process.env.QQ_COOKIE || null // 'uin=; qm_keyst=',
}

const playable = song => {
	const switchFlag = song['switch'].toString(2).split('')
	switchFlag.pop()
	switchFlag.reverse()
	const playFlag = switchFlag[0]
	const tryFlag = switchFlag[13]
	return ((playFlag == 1) || ((playFlag == 1) && (tryFlag == 1)))
}

const format = song => ({
	id: {song: song.mid, file: song.file.media_mid},
	name: song.name,
	duration: song.interval * 1000,
	album: {id: song.album.mid, name: song.album.name},
	artists: song.singer.map(({mid, name}) => ({id: mid, name}))
})

const search = info => {
	const url =
		'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' +
		'ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&' +
		'searchid=46804741196796149&t=0&aggr=1&cr=1&catZhida=1&lossless=0&' +
		'flag_qc=0&p=1&n=20&w=' + encodeURIComponent(info.keyword) + '&' +
		'g_tk=5381&jsonpCallback=MusicJsonCallback10005317669353331&loginUin=0&hostUin=0&' +
		'format=jsonp&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0'

	return request('GET', url)
	.then(response => response.jsonp())
	.then(jsonBody => {
		const list = jsonBody.data.song.list.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

const single = (id, format) => {
	// const classic = ['001yS0N33yPm1B', '000bog5B2DYgHN', '002bongo1BDtKz', '004RDW5Q2ol2jj', '001oEME64eXNbp', '001e9dH11YeXGp', '0021onBk2QNjBu', '001YoUs11jvsIK', '000SNxc91Mw3UQ', '002k94ea4379uy']
	// id = id || classic[Math.floor(classic.length * Math.random())]
	const uin = ((headers.cookie || '').match(/uin=(\d+)/) || [])[1] || '0'

	const concatenate = vkey => {
		if (!vkey) return Promise.reject()
		const 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]
		return `http://${host}/${format.join(id.file)}?vkey=${vkey}&uin=0&fromtag=8&guid=7332953645`
	}

	// const url =
	// 	'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' +
	// 	'?g_tk=0&loginUin=0&hostUin=0&format=json&inCharset=utf8' +
	// 	'&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0' +
	// 	'&cid=205361747&uin=0&guid=7332953645' +
	// 	'&songmid='+ id.song + '&filename='+ format.join(id.file)

	// return request('GET', url, headers)
	// .then(response => response.json())
	// .then(jsonBody => {
	// 	const {vkey} = jsonBody.data.items[0]
	// 	return concatenate(vkey)
	// })

	const url =
		'https://u.y.qq.com/cgi-bin/musicu.fcg?data=' +
		encodeURIComponent(JSON.stringify({
			// req: {
			// 	method: 'GetCdnDispatch',
			// 	module: 'CDN.SrfCdnDispatchServer',
			// 	param: {
			// 		calltype: 0,
			// 		guid: '7332953645',
			// 		userip: ''
			// 	}
			// },
			req_0: {
				module: 'vkey.GetVkeyServer',
				method: 'CgiGetVkey',
				param: {
					guid: '7332953645',
					loginflag: 1,
					filename: [format.join(id.file)],
					songmid: [id.song],
					songtype: [0],
					uin,
					platform: '20'
				}
			}
		}))

	return request('GET', url, headers)
	.then(response => response.json())
	.then(jsonBody => {
		const { sip, midurlinfo } = jsonBody.req_0.data
		// const vkey =
		// 	jsonBody.req_0.data.midurlinfo[0].vkey ||
		// 	(jsonBody.req_0.data.testfile2g.match(/vkey=(\w+)/) || [])[1]
		// return concatenate(vkey)
		return midurlinfo[0].purl ? sip[0] + midurlinfo[0].purl : Promise.reject()
	})
}

const track = id => {
	id.key = id.file
	return Promise.all(
		[['F000', '.flac'], ['M800', '.mp3'], ['M500', '.mp3']].slice((headers.cookie || typeof(window) !== 'undefined') ? (select.ENABLE_FLAC ? 0 : 1) : 2)
		.map(format => single(id, format).catch(() => null))
	)
	.then(result => result.find(url => url) || Promise.reject())
	.catch(() => insure().qq.track(id))

	// return request(
	// 	'POST', 'http://acc.music.qq.com/base/fcgi-bin/fcg_music_express_mobile2.fcg', {},
	// 	`<root>
	// 		<uid></uid><sid></sid><v>90</v><cv>70003</cv><ct>19</ct><OpenUDID>0</OpenUDID>
	// 		<mcc>460</mcc><mnc>01</mnc><chid>001</chid><webp>0</webp><gray>0</gray><patch>105</patch>
	// 		<jailbreak>0</jailbreak><nettype>2</nettype><qq>12345678</qq><authst></authst><localvip>2</localvip>
	// 		<cid>352</cid><platform>ios</platform><musicname>M800${id}.mp3</musicname><downloadfrom>0</downloadfrom>
	// 	</root>`.replace(/\s/, '')
	// )
	// .then(response => response.body(true))
	// .then(body => {
	// 	const xml = require('zlib').inflateSync(body.slice(5)).toString()
	// 	const focus = xml.match(/<item name="(.+)">(.+)<\/item>/)
	// 	return `http://streamoc.music.tc.qq.com/${focus[1]}?vkey=${focus[2]}&guid=0&uin=12345678&fromtag=6`
	// })

	// const url =
	// 	'https://i.y.qq.com/v8/playsong.html?ADTAG=newyqq.song&songmid=' + id

	// 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'}
	// return request('GET', url, mobile)
	// .then(response => response.body())
	// .then(body => {
	// 	const audio = body.match(/<audio[^>]+src="([^"]+)"[^>]*>/)
	// 	if (audio)
	// 		return audio[1].replace(/C400(\w+)\.m4a/, 'M500$1.mp3')
	// 	else
	// 		return Promise.reject()
	// })
}

const check = info => cache(search, info).then(track)

module.exports = {check, track}

================================================
FILE: src/provider/select.js
================================================
module.exports = list => list[0]

module.exports.ENABLE_FLAC = (process.env.ENABLE_FLAC || '').toLowerCase() === 'true'

================================================
FILE: src/provider/xiami.js
================================================
const cache = require('../cache')
const insure = require('./insure')
const select = require('./select')
const crypto = require('../crypto')
const request = require('../request')

const headers = {
	// 'origin': 'http://www.xiami.com/',
	// 'referer': 'http://www.xiami.com/'
	'referer': 'https://h.xiami.com/'
}

const format = song => ({
	id: song.song_id,
	name: song.song_name,
	album: {id: song.album_id, name: song.album_name},
	artists: [{id: song.artist_id, name: song.artist_name}]
})

const caesar = pattern => {
	const height = parseInt(pattern[0])
	pattern = pattern.slice(1)
	const width = Math.ceil(pattern.length / height)
	const unpad = height - (width * height - pattern.length)

	const matrix = Array.from(Array(height).keys()).map(i =>
		pattern.slice(i < unpad ? i * width : unpad * width + (i - unpad) * (width - 1)).slice(0, i < unpad ? width : width - 1)
	)

	const transpose = Array.from(Array(width).keys()).map(x =>
		Array.from(Array(height).keys()).map(y => matrix[y][x]).join('')
	)

	return unescape(transpose.join('')).replace(/\^/g, '0')
}

const token = () => {
	return request('GET', 'https://www.xiami.com')
	.then(response =>
		response.headers['set-cookie'].map(line => line.replace(/;.+$/, '')).reduce(
			(cookie, line) => (line = line.split(/\s*=\s*/).map(decodeURIComponent), Object.assign(cookie, {[line[0]]: line[1]})), {}
		)
	)
}

// const search = info => {
// 	return cache(token)
// 	.then(cookie => {
// 		const query = JSON.stringify({key: info.keyword, pagingVO: {page: 1, pageSize: 60}})
// 		const message = cookie['xm_sg_tk'].split('_')[0] + '_xmMain_/api/search/searchSongs_' + query
// 		return request('GET', 'https://www.xiami.com/api/search/searchSongs?_q=' + encodeURIComponent(query) + '&_s=' + crypto.md5.digest(message), {
// 			referer: 'https://www.xiami.com/search?key=' + encodeURIComponent(info.keyword),
// 			cookie: Object.keys(cookie).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(cookie[key])).join('; ')
// 		})
// 		.then(response => response.json())
// 		.then(jsonBody => {
// 			const matched = jsonBody.result.data.songs[0]
// 			if (matched)
// 				return matched.songId
// 			else
// 				return Promise.reject()
// 		})
// 	})
// }

const search = info => {
	const url =
		'http://api.xiami.com/web?v=2.0&app_key=1' +
		'&key=' + encodeURIComponent(info.keyword) + '&page=1' +
		'&limit=20&callback=jsonp&r=search/songs'

	return request('GET', url, headers)
	.then(response => response.jsonp())
	.then(jsonBody => {
		const list = jsonBody.data.songs.map(format)
		const matched = select(list, info)
		return matched ? matched.id : Promise.reject()
	})
}

// const track = id => {
// 	const url =
// 		'https://emumo.xiami.com/song/playlist/id/' + id +
// 		'/object_name/default/object_id/0/cat/json'

// 	return request('GET', url, headers)
// 	.then(response => response.json())
// 	.then(jsonBody => {
// 		if (jsonBody.data.trackList == null) {
// 			return Promise.reject()
// 		}
// 		else {
// 			const location = jsonBody.data.trackList[0].location
// 			const songUrl = 'http:' + caesar(location)
// 			return songUrl
// 		}
// 	})
// 	.then(origin => {
// 		const updated = origin.replace('m128', 'm320')
// 		return request('HEAD', updated)
// 		.then(response => response.statusCode == 200 ? updated : origin)
// 		.catch(() => origin)
// 	})
// 	.catch(() => insure().xiami.track(id))
// }

const track = id => {
	const url =
		'https://api.xiami.com/web?v=2.0&app_key=1' +
		'&id=' + id + '&callback=jsonp&r=song/detail'

	return request('GET', url, headers)
	.then(response => response.jsonp())
	.then(jsonBody =>
		jsonBody.data.song.listen_file || Promise.reject()
	)
	.catch(() => insure().xiami.track(id))
}

const check = info => cache(search, info).then(track)

module.exports = {check, track}

================================================
FILE: src/provider/youtube.js
================================================
const cache = require('../cache')
const request = require('../request')
const parse = query => (query || '').split('&').reduce((result, item) => (item = item.split('=').map(decodeURIComponent), Object.assign({}, result, {[item[0]]: item[1]})), {})

// const proxy = require('url').parse('http://127.0.0.1:1080')
const proxy = undefined
const key = process.env.YOUTUBE_KEY || null // YouTube Data API v3

const signature = (id = '-tKVN2mAKRI') => {
	const url =
		`https://www.youtube.com/watch?v=${id}`

	return request('GET', url, {}, null, proxy)
	.then(response => response.body())
	.then(body => {
		let assets = /"assets":{[^}]+}/.exec(body)[0]
		assets = JSON.parse(`{${assets}}`).assets
		return request('GET', 'https://youtube.com' + assets.js, {}, null, proxy).then(response => response.body())
	})
	.then(body => {
		const [_, funcArg, funcBody] = /function\((\w+)\)\s*{([^}]+split\(""\)[^}]+join\(""\))};/.exec(body)
		const helperName = /;(.+?)\..+?\(/.exec(funcBody)[1]
		const helperContent = new RegExp(`var ${helperName}={[\\s\\S]+?};`).exec(body)[0]
		return new Function([funcArg], helperContent + '\n' + funcBody)
	})
}

const apiSearch = info => {
	const url =
		`https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(info.keyword)}&type=video&key=${key}`

	return request('GET', url, {accept: 'application/json'}, null, proxy)
	.then(response => response.json())
	.then(jsonBody => {
		const matched = jsonBody.items[0]
		if (matched)
			return matched.id.videoId
		else
			return Promise.reject()
	})
}

const search = info => {
	const url =
		`https://www.youtube.com/results?search_query=${encodeURIComponent(info.keyword)}`

	return request('GET', url, {}, null, proxy)
	.then(response => response.body())
	.then(body => {
		const initialData = JSON.parse(body.match(/window\["ytInitialData"\]\s*=\s*([^;]+);/)[1])
		const matched = initialData.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]
		if (matched)
			return matched.videoRenderer.videoId
		else
			return Promise.reject()
	})
}

const track = id => {
	const url =
		`https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage`

	return request('GET', url, {}, null, proxy)
	.then(response => response.body())
	.then(body => JSON.parse(parse(body).player_response).streamingData)
	.then(streamingData => {
		const stream = streamingData.formats.concat(streamingData.adaptiveFormats)
		.find(format => format.itag === 140)
		// .filter(format => [249, 250, 140, 251].includes(format.itag)) // NetaseMusic PC client do not support webm format
		// .sort((a, b) => b.bitrate - a.bitrate)[0]
		const target = parse(stream.signatureCipher)
		return stream.url || (target.sp.includes('sig') ? cache(signature, undefined, 24 * 60 * 60 * 1000).then(sign => target.url + '&sig=' + sign(target.s)) : target.url)
	})
}

const check = info => cache(key ? apiSearch : search, info).then(track)

module.exports = {check, track}


================================================
FILE: src/request.js
================================================
const zlib = require('zlib')
const http = require('http')
const https = require('https')
const parse = require('url').parse

const translate = host => (global.hosts || {})[host] || host

const create = (url, proxy) => (((typeof(proxy) === 'undefined' ? global.proxy : proxy) || url).protocol === 'https:' ? https : http).request

const configure = (method, url, headers, proxy) => {
	headers = headers || {}
	proxy = typeof(proxy) === 'undefined' ? global.proxy : proxy
	if ('content-length' in headers) delete headers['content-length']

	const options = {}
	options._headers = headers
	if (proxy && url.protocol === 'https:') {
		options.method = 'CONNECT'
		options.headers = Object.keys(headers).reduce((result, key) => Object.assign(result, ['host', 'user-agent'].includes(key) && {[key]: headers[key]}), {})
	}
	else {
		options.method = method
		options.headers = headers
	}

	if (proxy) {
		options.hostname = translate(proxy.hostname)
		options.port = proxy.port || ((proxy.protocol === 'https:') ? 443 : 80)
		options.path = (url.protocol === 'https:') ? (translate(url.hostname) + ':' + (url.port || 443)) : ('http://' + translate(url.hostname) + url.path) 
	}
	else {
		options.hostname = translate(url.hostname)
		options.port = url.port || ((url.protocol === 'https:') ? 443 : 80)
		options.path = url.path
	}
	return options
}

const request = (method, url, headers, body, proxy) => {
	url = parse(url)
	headers = headers || {}
	const options = configure(method, url, Object.assign({
		'host': url.hostname,
		'accept': 'application/json, text/plain, */*',
		'accept-encoding': 'gzip, deflate',
		'accept-language': 'zh-CN,zh;q=0.9',
		'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'
	}, headers), proxy)

	return new Promise((resolve, reject) => {
		create(url, proxy)(options)
		.on('response', response => resolve(response))
		.on('connect', (_, socket) =>
			https.request({
				method: method,
				path: url.path,
				headers: options._headers,
				socket: socket,
				agent: false
			})
			.on('response', response => resolve(response))
			.on('error', error => reject(error))
			.end(body)
		)
		.on('error', error => reject(error))
		.end(options.method.toUpperCase() === 'CONNECT' ? undefined : body)
	})
	.then(response => {
		if (new Set([201, 301, 302, 303, 307, 308]).has(response.statusCode))
			return request(method, url.resolve(response.headers.location || url.href), (delete headers.host, headers), body, proxy)
		else
			return Object.assign(response, {url: url, body: raw => read(response, raw), json: () => json(response), jsonp: () => jsonp(response)})
	})
}

const read = (connect, raw) =>
	new Promise((resolve, reject) => {
		const chunks = []
		connect
		.on('data', chunk => chunks.push(chunk))
		.on('end', () => resolve(Buffer.concat(chunks)))
		.on('error', error => reject(error))
	})
	.then(buffer => {
		buffer = (buffer.length && ['gzip', 'deflate'].includes(connect.headers['content-encoding'])) ? zlib.unzipSync(buffer) : buffer
		return raw ? buffer : buffer.toString()
	})

const json = connect => read(connect, false).then(body => JSON.parse(body))
const jsonp = connect => read(connect, false).then(body => JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length)))

request.read = read
request.create = create
request.translate = translate
request.configure = configure

module.exports = request

================================================
FILE: src/server.js
================================================
const fs = require('fs')
const net = require('net')
const path = require('path')
const parse = require('url').parse

const sni = require('./sni')
const hook = require('./hook')
const request = require('./request')

const proxy = {
	core: {
		mitm: (req, res) => {
			if (req.url == '/proxy.pac') {
				const url = parse('http://' + req.headers.host)
				res.writeHead(200, {'Content-Type': 'application/x-ns-proxy-autoconfig'})
				res.end(`
					function FindProxyForURL(url, host) {
						if (${Array.from(hook.target.host).map(host => (`host == '${host}'`)).join(' || ')}) {
							return 'PROXY ${url.hostname}:${url.port || 80}'
						}
						return 'DIRECT'
					}
				`)
			}
			else {
				const ctx = {res, req}
				Promise.resolve()
				.then(() => proxy.protect(ctx))
				.then(() => proxy.authenticate(ctx))
				.then(() => hook.request.before(ctx))
				.then(() => proxy.filter(ctx))
				.then(() => proxy.log(ctx))
				.then(() => proxy.mitm.request(ctx))
				.then(() => hook.request.after(ctx))
				.then(() => proxy.mitm.response(ctx))
				.catch(() => proxy.mitm.close(ctx))
			}
		},
		tunnel: (req, socket, head) => {
			const ctx = {req, socket, head}
			Promise.resolve()
			.then(() => proxy.protect(ctx))
			.then(() => proxy.authenticate(ctx))
			.then(() => hook.connect.before(ctx))
			.then(() => proxy.filter(ctx))
			.then(() => proxy.log(ctx))
			.then(() => proxy.tunnel.connect(ctx))
			.then(() => proxy.tunnel.dock(ctx))
			.then(() => hook.negotiate.before(ctx))
			.then(() => proxy.tunnel.pipe(ctx))
			.catch(() => proxy.tunnel.close(ctx))
		}
	},
	abort: (socket, from) => {
		// console.log('call abort', from)
		if (socket) socket.end()
		if (socket && !socket.destroyed) socket.destroy()
	},
	protect: ctx => {
		const {req, res, socket} = ctx
		if (req) req.on('error', () => proxy.abort(req.socket, 'req'))
		if (res) res.on('error', () => proxy.abort(res.socket, 'res'))
		if (socket) socket.on('error', () => proxy.abort(socket, 'socket'))
	},
	log: ctx => {
		const {req, socket, decision} = ctx
		const mark = {close: '|', blank: '-', proxy: '>'}[decision] || '>'
		if (socket)
			console.log('TUNNEL', mark, req.url)
		else
			console.log('MITM', mark, parse(req.url).host, req.socket.encrypted ? '(ssl)' : '')
	},
	authenticate: ctx => {
		const {req, res, socket} = ctx
		const credential = Buffer.from((req.headers['proxy-authorization'] || '').split(/\s+/).pop() || '', 'base64').toString()
		if ('proxy-authorization' in req.headers) delete req.headers['proxy-authorization']
		if (server.authentication && credential != server.authentication && (socket || req.url.startsWith('http://'))) {
			if (socket)
				socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="realm"\r\n\r\n')
			else
				res.writeHead(407, {'proxy-authenticate': 'Basic realm="realm"'})
			return Promise.reject(ctx.error = 'authenticate')
		}
	},
	filter: ctx => {
		if (ctx.decision || ctx.req.local) return
		const url = parse((ctx.socket ? 'https://' : '') + ctx.req.url)
		const match = pattern => url.href.search(new RegExp(pattern, 'g')) != -1
		try {
			const allow = server.whitelist.some(match)
			const deny = server.blacklist.some(match)
			// console.log('allow', allow, 'deny', deny)
			if (!allow && deny) {
				return Promise.reject(ctx.error = 'filter')
			}
		}
		catch(error) {
			ctx.error = error
		}
	},
	mitm: {
		request: ctx => new Promise((resolve, reject) => {
			if (ctx.decision === 'close') return reject(ctx.error = ctx.decision)
			const {req} = ctx
			const url = parse(req.url)
			const options = request.configure(req.method, url, req.headers)
			ctx.proxyReq = request.create(url)(options)
			.on('response', proxyRes => resolve(ctx.proxyRes = proxyRes))
			.on('error', error => reject(ctx.error = error))
			req.readable ? req.pipe(ctx.proxyReq) : ctx.proxyReq.end(req.body)
		}),
		response: ctx => {
			const {res, proxyRes} = ctx
			proxyRes.on('error', () => proxy.abort(proxyRes.socket, 'proxyRes'))
			res.writeHead(proxyRes.statusCode, proxyRes.headers)
			proxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body)
		},
		close: ctx => {
			proxy.abort(ctx.res.socket, 'mitm')
		}
	},
	tunnel: {
		connect: ctx => new Promise((resolve, reject) => {
			if (ctx.decision === 'close') return reject(ctx.error = ctx.decision)
			const {req} = ctx
			const url = parse('https://' + req.url)
			if (global.proxy && !req.local) {
				const options = request.configure(req.method, url, req.headers)
				request.create(proxy)(options)
				.on('connect', (_, proxySocket) => resolve(ctx.proxySocket = proxySocket))
				.on('error', error => reject(ctx.error = error))
				.end()
			}
			else {
				const proxySocket = net.connect(url.port || 443, request.translate(url.hostname))
				.on('connect', () => resolve(ctx.proxySocket = proxySocket))
				.on('error', error => reject(ctx.error = error))
			}
		}),
		dock: ctx => new Promise(resolve => {
			const {req, head, socket} = ctx
			socket
			.once('data', data => resolve(ctx.head = Buffer.concat([head, data])))
			.write(`HTTP/${req.httpVersion} 200 Connection established\r\n\r\n`)
		}).then(data => ctx.socket.sni = sni(data)).catch(() => {}),
		pipe: ctx => {
			if (ctx.decision === 'blank') return Promise.reject(ctx.error = ctx.decision)
			const {head, socket, proxySocket} = ctx
			proxySocket.on('error', () => proxy.abort(ctx.proxySocket, 'proxySocket'))
			proxySocket.write(head)
			socket.pipe(proxySocket)
			proxySocket.pipe(socket)
		},
		close: ctx => {
			proxy.abort(ctx.socket, 'tunnel')
		}
	}
}

const options = {
	key: fs.readFileSync(path.join(__dirname, '..', 'server.key')),
	cert: fs.readFileSync(path.join(__dirname, '..', 'server.crt'))
}

const server = {
	http: require('http').createServer().on('request', proxy.core.mitm).on('connect', proxy.core.tunnel),
	https: require('https').createServer(options).on('request', proxy.core.mitm).on('connect', proxy.core.tunnel)
}

server.whitelist = []
server.blacklist = ['://127\\.\\d+\\.\\d+\\.\\d+', '://localhost']
server.authentication = null

module.exports = server

================================================
FILE: src/sni.js
================================================
// Thanks to https://github.com/buschtoens/sni

module.exports = data => {
	let end = data.length
	let pointer = 5 + 1 + 3 + 2 + 32
	const nan = (number = pointer) => isNaN(number)

	if (pointer + 1 > end || nan()) return null
	pointer += 1 + data[pointer]

	if (pointer + 2 > end || nan()) return null
	pointer += 2 + data.readInt16BE(pointer)

	if (pointer + 1 > end || nan()) return null
	pointer += 1 + data[pointer]

	if (pointer + 2 > end || nan()) return null
	const extensionsLength = data.readInt16BE(pointer)
	pointer += 2
	const extensionsEnd = pointer + extensionsLength

	if (extensionsEnd > end || nan(extensionsEnd)) return null
	end = extensionsEnd

	while (pointer + 4 <= end || nan()) {
		const extensionType = data.readInt16BE(pointer)
		const extensionSize = data.readInt16BE(pointer + 2)
		pointer += 4
		if (extensionType !== 0) {
			pointer += extensionSize
			continue
		}
		if (pointer + 2 > end || nan()) return null
		const nameListLength = data.readInt16BE(pointer)
		pointer += 2
		if (pointer + nameListLength > end) return null

		while (pointer + 3 <= end || nan()) {
			const nameType = data[pointer]
			const nameLength = data.readInt16BE(pointer + 1)
			pointer += 3
			if (nameType !== 0) {
				pointer += nameLength
				continue
			}
			if (pointer + nameLength > end || nan()) return null
			return data.toString('ascii', pointer, pointer + nameLength)
		}
	}

	return null
}
Download .txt
gitextract_m6vtz5ok/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.js
├── bridge.js
├── ca.crt
├── docker-compose.yml
├── endpoint.worker.js
├── package.json
├── server.crt
├── server.key
└── src/
    ├── app.js
    ├── bridge.js
    ├── browser/
    │   ├── README.md
    │   ├── background.html
    │   ├── background.js
    │   ├── convert.js
    │   ├── crypto.js
    │   ├── inject.js
    │   ├── manifest.json
    │   ├── request.js
    │   └── script.js
    ├── cache.js
    ├── cli.js
    ├── crypto.js
    ├── hook.js
    ├── kwDES.js
    ├── provider/
    │   ├── baidu.js
    │   ├── find.js
    │   ├── insure.js
    │   ├── joox.js
    │   ├── kugou.js
    │   ├── kuwo.js
    │   ├── match.js
    │   ├── migu.js
    │   ├── netease.js
    │   ├── qq.js
    │   ├── select.js
    │   ├── xiami.js
    │   └── youtube.js
    ├── request.js
    ├── server.js
    └── sni.js
Download .txt
SYMBOL INDEX (1 symbols across 1 files)

FILE: src/kwDES.js
  constant SECRET_KEY (line 286) | const SECRET_KEY = Buffer.from('ylzsxkwm')
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (111K chars).
[
  {
    "path": ".dockerignore",
    "chars": 132,
    "preview": ".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.*"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1636,
    "preview": "name: publish\n\non:\n  push:\n    tags:\n       - '*'\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    env:\n      REPOSITORY:"
  },
  {
    "path": ".gitignore",
    "chars": 1026,
    "preview": "# 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"
  },
  {
    "path": ".npmignore",
    "chars": 85,
    "preview": ".npmignore\n.gitignore\n.dockerignore\n\nDockerfile*\n*.yml\n\nsrc/browser/\nca.*\n*.worker.js"
  },
  {
    "path": "Dockerfile",
    "chars": 269,
    "preview": "FROM alpine\nRUN apk add --update nodejs npm --repository=http://dl-cdn.alpinelinux.org/alpine/latest-stable/main/\n\nENV N"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2018 Nzix\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof th"
  },
  {
    "path": "README.md",
    "chars": 5785,
    "preview": "<img src=\"https://user-images.githubusercontent.com/26399680/47980314-0e3f1700-e102-11e8-8857-e3436ecc8beb.png\" alt=\"log"
  },
  {
    "path": "app.js",
    "chars": 40,
    "preview": "#!/usr/bin/env node\nrequire('./src/app')"
  },
  {
    "path": "bridge.js",
    "chars": 43,
    "preview": "#!/usr/bin/env node\nrequire('./src/bridge')"
  },
  {
    "path": "ca.crt",
    "chars": 1261,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIJAKX8LdIETDklMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAkNOMSQwIgYDVQQDDBtVbmJ"
  },
  {
    "path": "docker-compose.yml",
    "chars": 160,
    "preview": "version: '3'\n\nservices:\n  unblockneteasemusic:\n    image: nondanee/unblockneteasemusic\n    environment:\n      NODE_ENV: "
  },
  {
    "path": "endpoint.worker.js",
    "chars": 735,
    "preview": "addEventListener('fetch', event => {\n  event.respondWith(handleRequest(event.request))\n})\n\nconst pattern = /^\\/package\\/"
  },
  {
    "path": "package.json",
    "chars": 580,
    "preview": "{\n  \"name\": \"@nondanee/unblockneteasemusic\",\n  \"version\": \"0.25.3\",\n  \"description\": \"Revive unavailable songs for Netea"
  },
  {
    "path": "server.crt",
    "chars": 1298,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIDkjCCAnqgAwIBAgIJAK/bIUIlE36LMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAkNOMSQwIgYDVQQDDBtVbmJ"
  },
  {
    "path": "server.key",
    "chars": 1675,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuhsQLKnASh7xWoz8Um8FFHenZP5lZ2IxI+vB+JEmvmns4y1Y\nGUu94IFXzn5Hd96J1htBNeW"
  },
  {
    "path": "src/app.js",
    "chars": 4469,
    "preview": "#!/usr/bin/env node\n\nconst package = require('../package.json')\nconst config = require('./cli.js')\n.program({name: packa"
  },
  {
    "path": "src/bridge.js",
    "chars": 1176,
    "preview": "#!/usr/bin/env node\nconst cache = require('./cache')\nconst parse = require('url').parse\nrequire('./provider/insure').dis"
  },
  {
    "path": "src/browser/README.md",
    "chars": 899,
    "preview": "# Web Extension Port\n\nFor test\n\n## Implementation\n\n- Convert node module to ES6 module which can be directly executed in"
  },
  {
    "path": "src/browser/background.html",
    "chars": 215,
    "preview": "<script src=\"https://cdn.jsdelivr.net/npm/crypto-js/crypto-js.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/np"
  },
  {
    "path": "src/browser/background.js",
    "chars": 1479,
    "preview": "import match from './provider/match.js'\nconst self = chrome.runtime.id\n\nchrome.runtime.onMessageExternal.addListener((re"
  },
  {
    "path": "src/browser/convert.js",
    "chars": 1633,
    "preview": "const fs = require('fs')\nconst path = require('path')\n\nconst importReplacer = (match, state, alias, file) => {\n\tfile = f"
  },
  {
    "path": "src/browser/crypto.js",
    "chars": 1481,
    "preview": "\nconst bodyify = object => Object.entries(object).map(entry => entry.map(encodeURIComponent).join('=')).join('&')\n\nconst"
  },
  {
    "path": "src/browser/inject.js",
    "chars": 2283,
    "preview": "(() => {\n\tconst remote = 'oleomikdicccalekkpcbfgdmpjehnpkp'\n\tconst remoteMatch = id => new Promise(resolve => {\n\t\tchrome"
  },
  {
    "path": "src/browser/manifest.json",
    "chars": 561,
    "preview": "{\n\t\"name\": \"UnblockNeteaseMusic\",\n\t\"description\": \"For test (es6 only)\",\n\t\"version\": \"0.1\",\n\t\"background\": {\n\t\t\"page\": \""
  },
  {
    "path": "src/browser/request.js",
    "chars": 1098,
    "preview": "export default (method, url, headers, body) => new Promise((resolve, reject) => {\n\theaders = headers || {}\n\tconst xhr = "
  },
  {
    "path": "src/browser/script.js",
    "chars": 223,
    "preview": "(() => {\n\tlet script = (document.head || document.documentElement).appendChild(document.createElement('script'))\n\tscript"
  },
  {
    "path": "src/cache.js",
    "chars": 994,
    "preview": "const collector = (job, cycle) =>\n\tsetTimeout(() => {\n\t\tlet keep = false\n\t\tObject.keys(job.cache || {})\n\t\t.forEach(key ="
  },
  {
    "path": "src/cli.js",
    "chars": 6292,
    "preview": "const cli = {\n\twidth: 80,\n\t_program: {},\n\t_options: [],\n\tprogram: (information = {}) => {\n\t\tcli._program = information\n\t"
  },
  {
    "path": "src/crypto.js",
    "chars": 4050,
    "preview": "'use strict'\n\nconst crypto = require('crypto')\nconst parse = require('url').parse\nconst bodyify = require('querystring')"
  },
  {
    "path": "src/hook.js",
    "chars": 11850,
    "preview": "const cache = require('./cache')\nconst parse = require('url').parse\nconst crypto = require('./crypto')\nconst request = r"
  },
  {
    "path": "src/kwDES.js",
    "chars": 8158,
    "preview": "/*\n\tThanks to\n\thttps://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py\n\thttps://github.com/Levi233/MusicPlayer/blo"
  },
  {
    "path": "src/provider/baidu.js",
    "chars": 1376,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst request = "
  },
  {
    "path": "src/provider/find.js",
    "chars": 1332,
    "preview": "const cache = require('../cache')\nconst request = require('../request')\n\nconst filter = (object, keys) => Object.keys(ob"
  },
  {
    "path": "src/provider/insure.js",
    "chars": 672,
    "preview": "const request = require('../request')\nconst host = null // 'http://localhost:9000'\n\nmodule.exports = () => {\n\tconst prox"
  },
  {
    "path": "src/provider/joox.js",
    "chars": 1830,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = r"
  },
  {
    "path": "src/provider/kugou.js",
    "chars": 1512,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = r"
  },
  {
    "path": "src/provider/kuwo.js",
    "chars": 2948,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = r"
  },
  {
    "path": "src/provider/match.js",
    "chars": 3204,
    "preview": "const find = require('./find')\nconst request = require('../request')\n\nconst provider = {\n\tnetease: require('./netease'),"
  },
  {
    "path": "src/provider/migu.js",
    "chars": 1710,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = r"
  },
  {
    "path": "src/provider/netease.js",
    "chars": 771,
    "preview": "const cache = require('../cache')\nconst crypto = require('../crypto')\nconst request = require('../request')\n\nconst searc"
  },
  {
    "path": "src/provider/qq.js",
    "chars": 5553,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst request = "
  },
  {
    "path": "src/provider/select.js",
    "chars": 119,
    "preview": "module.exports = list => list[0]\n\nmodule.exports.ENABLE_FLAC = (process.env.ENABLE_FLAC || '').toLowerCase() === 'true'"
  },
  {
    "path": "src/provider/xiami.js",
    "chars": 3812,
    "preview": "const cache = require('../cache')\nconst insure = require('./insure')\nconst select = require('./select')\nconst crypto = r"
  },
  {
    "path": "src/provider/youtube.js",
    "chars": 3006,
    "preview": "const cache = require('../cache')\nconst request = require('../request')\nconst parse = query => (query || '').split('&')."
  },
  {
    "path": "src/request.js",
    "chars": 3435,
    "preview": "const zlib = require('zlib')\nconst http = require('http')\nconst https = require('https')\nconst parse = require('url').pa"
  },
  {
    "path": "src/server.js",
    "chars": 6098,
    "preview": "const fs = require('fs')\nconst net = require('net')\nconst path = require('path')\nconst parse = require('url').parse\n\ncon"
  },
  {
    "path": "src/sni.js",
    "chars": 1413,
    "preview": "// Thanks to https://github.com/buschtoens/sni\n\nmodule.exports = data => {\n\tlet end = data.length\n\tlet pointer = 5 + 1 +"
  }
]

About this extraction

This page contains the full source code of the nondanee/UnblockNeteaseMusic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (99.1 KB), approximately 34.8k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!