ZSCGR commited on
Commit
5ed446f
·
verified ·
1 Parent(s): 743bff1

Upload folder using huggingface_hub

Browse files
.dockerignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .vscode
4
+ .vercel
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/1.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/test.png filter=lfs diff=lfs merge=lfs -text
.github/workflows/publish.yml ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish
2
+ on:
3
+ push:
4
+ branches:
5
+ - "main"
6
+ tags:
7
+ - "v*"
8
+
9
+ workflow_dispatch:
10
+ inputs:
11
+ docker:
12
+ description: "Deploy to DockerHub"
13
+ required: true
14
+ type: boolean
15
+ default: false
16
+ deno:
17
+ description: "Deploy to deno"
18
+ required: true
19
+ type: boolean
20
+ default: false
21
+
22
+ jobs:
23
+ unit-test:
24
+ runs-on: ubuntu-latest
25
+
26
+ strategy:
27
+ matrix:
28
+ node-version: ['18.x', '19.x', '20.x', '21.x']
29
+
30
+ steps:
31
+ - uses: actions/checkout@v3
32
+ - uses: actions/setup-node@v3
33
+ with:
34
+ node-version: ${{ matrix.node-version }}
35
+
36
+ - run: npm i
37
+ - run: npm run build:all
38
+ - run: npm test
39
+
40
+ test-docker:
41
+ runs-on: ubuntu-latest
42
+ needs: [unit-test]
43
+
44
+ steps:
45
+ - uses: actions/checkout@v3
46
+
47
+ - name: Set up QEMU
48
+ uses: docker/setup-qemu-action@v2
49
+
50
+ - name: Set up Docker Buildx
51
+ uses: docker/setup-buildx-action@v2
52
+
53
+ - name: Build docker image
54
+ uses: docker/build-push-action@v3
55
+ with:
56
+ load: true
57
+ push: false
58
+ tags: meting-api:latest
59
+ context: .
60
+
61
+ - name: Test Docker image
62
+ run: bash scripts/docker/test-docker.sh
63
+ env:
64
+ TAG: latest
65
+
66
+ - name: Export Docker image
67
+ run: docker save meting-api:latest | gzip -1cf > meting.tar.gz
68
+
69
+ - name: Upload Docker image
70
+ uses: actions/upload-artifact@v4
71
+ with:
72
+ name: meting.tar.gz
73
+ path: meting.tar.gz
74
+ retention-days: 1
75
+
76
+ upload-art:
77
+ needs: [test-docker]
78
+ runs-on: ubuntu-latest
79
+
80
+ steps:
81
+ - uses: actions/checkout@v3
82
+ - uses: actions/setup-node@v3
83
+ with:
84
+ node-version: 18
85
+
86
+ - run: npm i
87
+ - run: npm run build:all
88
+
89
+ - uses: actions/upload-artifact@v4
90
+ with:
91
+ name: cloudflare-workers.js
92
+ path: dist/cloudflare-workers.js
93
+ retention-days: 1
94
+ - uses: actions/upload-artifact@v4
95
+ with:
96
+ name: deno.js
97
+ path: dist/deno.js
98
+ retention-days: 1
99
+
100
+ # deploy-to-deno:
101
+ # needs: [upload-art]
102
+ # if: ${{ inputs.deno || github.event_name == 'push' }}
103
+ # runs-on: ubuntu-latest
104
+ # permissions:
105
+ # id-token: write
106
+ # contents: read
107
+
108
+ # steps:
109
+ # - uses: actions/download-artifact@v3
110
+ # with:
111
+ # name: deno.js
112
+ # - name: Upload to Deno Deploy
113
+ # uses: denoland/deployctl@v1
114
+ # with:
115
+ # project: meting
116
+ # entrypoint: deno.js
117
+
118
+ release-docker:
119
+ needs: [test-docker]
120
+ if: ${{ inputs.docker || github.event_name == 'push' }}
121
+ runs-on: ubuntu-latest
122
+
123
+ steps:
124
+ - uses: actions/checkout@v3
125
+
126
+ - name: Login to DockerHub
127
+ uses: docker/login-action@v2
128
+ with:
129
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
130
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
131
+
132
+ - name: Docker meta
133
+ id: meta
134
+ uses: docker/metadata-action@v4
135
+ with:
136
+ images: ${{ secrets.DOCKERHUB_USERNAME }}/meting-api
137
+
138
+ - name: Push docker image
139
+ uses: docker/build-push-action@v3
140
+ with:
141
+ push: true
142
+ tags: |
143
+ ${{ secrets.DOCKERHUB_USERNAME }}/meting-api:latest
144
+ ${{ steps.meta.outputs.tags }}
145
+ labels: ${{ steps.meta.outputs.labels }}
146
+ context: .
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .vscode
4
+ .vercel
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:23-alpine3.20
2
+
3
+ WORKDIR /app
4
+ COPY . /app
5
+
6
+ ARG UID
7
+ ARG GID
8
+ ARG PORT
9
+
10
+ ENV UID=${UID:-1010}
11
+ ENV GID=${GID:-1010}
12
+ ENV PORT=${PORT:-3000}
13
+
14
+ RUN addgroup -g ${GID} --system meting \
15
+ && adduser -G meting --system -D -s /bin/sh -u ${UID} meting
16
+
17
+ RUN npm i
18
+
19
+ RUN chown -R meting:meting /app
20
+ USER meting
21
+
22
+ EXPOSE ${PORT}
23
+
24
+ CMD ["node", "/app/node.js"]
README.md CHANGED
@@ -1,10 +1,15 @@
1
  ---
2
- title: Meting API
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: "Meting-API"
3
+ emoji: "🚀"
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 3000
8
  ---
9
 
10
+ ### 🚀 一键部署
11
+ [![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](https://github.com/kfcx/HFSpaceDeploy)
12
+
13
+ 本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署
14
+
15
+
api/index.js ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import app from "../app.js"
2
+
3
+ async function writeReadableStreamToWritable(stream, writable) {
4
+ let reader = stream.getReader()
5
+
6
+ async function read() {
7
+ let { done, value } = await reader.read()
8
+
9
+ if (done) {
10
+ writable.end()
11
+ return
12
+ }
13
+
14
+ writable.write(value)
15
+
16
+ await read()
17
+ }
18
+
19
+ try {
20
+ await read()
21
+ } catch (error) {
22
+ writable.destroy(error)
23
+ throw error
24
+ }
25
+ }
26
+
27
+ class NodeResponse extends Response {
28
+ get headers() {
29
+ return super.headers
30
+ }
31
+
32
+ clone() {
33
+ return super.clone()
34
+ }
35
+ }
36
+ const getRequestListener = (fetchCallback) => {
37
+ return async (incoming, outgoing) => {
38
+ const method = incoming.method || 'GET'
39
+ const url = `http://${incoming.headers.host}${incoming.url}`
40
+
41
+ const headerRecord = {}
42
+ const len = incoming.rawHeaders.length
43
+ for (let i = 0; i < len; i++) {
44
+ if (i % 2 === 0) {
45
+ const key = incoming.rawHeaders[i]
46
+ headerRecord[key] = incoming.rawHeaders[i + 1]
47
+ }
48
+ }
49
+
50
+ const init = {
51
+ method: method,
52
+ headers: headerRecord,
53
+ }
54
+
55
+ if (!(method === 'GET' || method === 'HEAD')) {
56
+ const buffers = []
57
+ for await (const chunk of incoming) {
58
+ buffers.push(chunk)
59
+ }
60
+ const buffer = Buffer.concat(buffers)
61
+ init['body'] = buffer
62
+ }
63
+
64
+ let res
65
+
66
+ try {
67
+ res = (await fetchCallback(new Request(url.toString(), init)))
68
+ } catch {
69
+ res = new NodeResponse(null, { status: 500 })
70
+ }
71
+
72
+ const contentType = res.headers.get('content-type') || ''
73
+ const contentEncoding = res.headers.get('content-encoding')
74
+
75
+ for (const [k, v] of res.headers) {
76
+ if (k === 'set-cookie') {
77
+ outgoing.setHeader(k, res.headers.getAll(k))
78
+ } else {
79
+ outgoing.setHeader(k, v)
80
+ }
81
+ }
82
+ outgoing.statusCode = res.status
83
+
84
+ if (res.body) {
85
+ if (!contentEncoding && contentType.startsWith('text')) {
86
+ outgoing.end(await res.text())
87
+ } else if (!contentEncoding && contentType.startsWith('application/json')) {
88
+ outgoing.end(await res.text())
89
+ } else {
90
+ await writeReadableStreamToWritable(res.body, outgoing)
91
+ }
92
+ } else {
93
+ outgoing.end()
94
+ }
95
+ }
96
+ }
97
+
98
+ export default getRequestListener(app.fetch)
app.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import api from './src/service/api.js'
2
+ import { handler } from './src/template.js'
3
+ import { Hono } from 'hono'
4
+ import { logger } from 'hono/logger'
5
+ import { cors } from 'hono/cors'
6
+ import config from './src/config.js'
7
+ import { get_runtime, get_url } from './src/util.js'
8
+
9
+ const app = new Hono()
10
+
11
+ app.use('*', cors())
12
+ app.use('*', logger())
13
+ app.get('/api', api)
14
+ app.get('/test', handler)
15
+ app.get('/', (c) => {
16
+
17
+ return c.html(`
18
+ <html>
19
+ <head>
20
+ <title>Meting正在运行</title>
21
+ </head>
22
+ <body>
23
+ <h1>Meting API</h1>
24
+ <p>
25
+ <a href="https://github.com/xizeyoupan/Meting-API" style="text-decoration: none;">
26
+ <img alt="Static Badge" src="https://img.shields.io/badge/Github-Meting-green">
27
+ <img alt="GitHub forks" src="https://img.shields.io/github/forks/xizeyoupan/Meting-API">
28
+ <img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/xizeyoupan/Meting-API">
29
+ </a>
30
+ </p>
31
+
32
+ <p>当前版本:1.1.2</p>
33
+ <p>当前运行环境:${get_runtime()}</p>
34
+ <p>当前时间:${new Date()}</p>
35
+ <p>内部端口:${config.PORT}</p>
36
+ <p>部署在大陆:${config.OVERSEAS ? '否' : '是'}</p>
37
+ <p>当前地址:<a>${c.req.url}</a></p>
38
+ <p>实际地址:<a>${get_url(c)}</a></p>
39
+ <p>测试地址:<a href="${get_url(c) + 'test'}">${get_url(c) + 'test'}</a></p>
40
+ <p>api地址:<a href="${get_url(c) + 'api'}">${get_url(c) + 'api'}</a></p>
41
+
42
+ </body>
43
+ </html>`
44
+ )
45
+ })
46
+
47
+ export default app
assets/1.png ADDED

Git LFS Details

  • SHA256: 22d491c8eef96c73ce2248ee72e5b1d275c2cdfec1a5bfae8e5142b4e4edd97a
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
assets/2.png ADDED
assets/3.png ADDED
assets/4.png ADDED
assets/5.png ADDED
assets/6.png ADDED
assets/7.png ADDED
assets/test.png ADDED

Git LFS Details

  • SHA256: 8630bfcc5f969edc83254a9a24713d65c2b895fe21876ae6990247eb885e522e
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
deno.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import config from './src/config.js'
2
+ import app from './app.js'
3
+ import { serve } from 'https://deno.land/std/http/server.ts'
4
+
5
+
6
+ serve(app.fetch, { port: config.PORT })
esbuild.config.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import esbuild from 'esbuild';
2
+ import resolve from 'esbuild-plugin-resolve';
3
+ import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill'
4
+ import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'
5
+ import textReplace from 'esbuild-plugin-text-replace'
6
+
7
+ await esbuild.build({
8
+ entryPoints: ['./app.js'],
9
+ bundle: true,
10
+ format: 'esm',
11
+ outfile: './dist/cloudflare-workers-min.js',
12
+ external: [],
13
+ plugins: [
14
+ resolve({
15
+ crypto: 'crypto-browserify'
16
+ }),
17
+ NodeGlobalsPolyfillPlugin({
18
+ process: true,
19
+ buffer: true,
20
+ }),
21
+ NodeModulesPolyfillPlugin(),
22
+ ],
23
+ minify: true,
24
+ });
25
+
26
+ await esbuild.build({
27
+ entryPoints: ['./app.js'],
28
+ bundle: true,
29
+ format: 'esm',
30
+ outfile: './dist/cloudflare-workers.js',
31
+ external: [],
32
+ plugins: [
33
+ resolve({
34
+ crypto: 'crypto-browserify'
35
+ }),
36
+ NodeGlobalsPolyfillPlugin({
37
+ process: true,
38
+ buffer: true,
39
+ }),
40
+ NodeModulesPolyfillPlugin(),
41
+ ],
42
+ });
43
+
44
+ await esbuild.build({
45
+ entryPoints: ['./deno.js'],
46
+ bundle: true,
47
+ format: 'esm',
48
+ outfile: './dist/deno.js',
49
+ external: [],
50
+ plugins: [
51
+ textReplace({
52
+ include: new RegExp("src/providers/netease/crypto\.js"),
53
+ pattern: [
54
+ ["import crypto from 'crypto-browserify'", "import crypto from 'https://esm.sh/crypto-browserify@3.12.0'"],
55
+ ["import { Buffer } from 'buffer/index.js'","import { Buffer } from 'https://esm.sh/buffer@6.0.3'"]
56
+ ]
57
+ }),
58
+ resolve({
59
+ crypto: 'crypto-browserify'
60
+ }),
61
+ NodeGlobalsPolyfillPlugin({
62
+ process: true,
63
+ buffer: true,
64
+ }),
65
+ NodeModulesPolyfillPlugin(),
66
+
67
+ ],
68
+ // minify: true,
69
+ });
node.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { serve } from '@hono/node-server'
2
+
3
+ import app from './app.js'
4
+ import config from './src/config.js'
5
+
6
+ serve({
7
+ fetch: app.fetch,
8
+ port: config.PORT
9
+ })
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "meting-backend-js",
3
+ "version": "1.1.2",
4
+ "description": "",
5
+ "main": "node.js",
6
+ "scripts": {
7
+ "build:all": "node esbuild.config.js",
8
+ "start:node": "node node.js",
9
+ "start:deno": "deno run --allow-net --allow-env dist/deno.js",
10
+ "test": "vitest run",
11
+ "patch": "npm version patch && git push origin main && git push origin --tags",
12
+ "minor": "npm version minor && git push origin main && git push origin --tags",
13
+ "major": "npm version major && git push origin main && git push origin --tags"
14
+ },
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
18
+ "type": "module",
19
+ "author": "xizeyoupan",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "esbuild-plugin-resolve": "^1.0.3",
23
+ "esbuild-plugin-text-replace": "^1.2.0",
24
+ "vitest": "^0.29.2"
25
+ },
26
+ "dependencies": {
27
+ "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
28
+ "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
29
+ "@hono/node-server": "^1.8.2",
30
+ "buffer": "^6.0.3",
31
+ "crypto-browserify": "^3.12.0",
32
+ "esbuild": "0.17.5",
33
+ "hono": "^4.1.0",
34
+ "nanoid": "^4.0.0"
35
+ }
36
+ }
scripts/docker/test-docker.sh ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ MAX_RETRIES=10
4
+
5
+ # Try running the docker and get the output
6
+ # then try getting /api
7
+
8
+ docker run -d -p 3000:3000 meting-api:${TAG}
9
+
10
+ if [[ $? -ne 0 ]]
11
+ then
12
+ echo "Fail to run docker"
13
+ exit 1
14
+ fi
15
+
16
+ RETRY=1
17
+
18
+ HTTP_CODE=$(curl -m 10 localhost:3000/api -w "%{http_code}" -o /dev/null)
19
+ while [[ $? -ne 0 || "$HTTP_CODE" -ne 200 ]] && [[ $RETRY -lt $MAX_RETRIES ]]; do
20
+ echo "HTTP_CODE: ${HTTP_CODE}"
21
+ sleep 5
22
+ ((RETRY++))
23
+ echo "RETRY: ${RETRY}"
24
+ HTTP_CODE=$(curl -m 10 localhost:3000/api -w "%{http_code}" -o /dev/null)
25
+ done
26
+
27
+ if [[ $RETRY -gt $MAX_RETRIES ]]; then
28
+ echo "Unable to run, aborted"
29
+ exit 1
30
+ else
31
+ if [[ $HTTP_CODE -ne 200 ]]; then
32
+ echo "Api error"
33
+ exit 1
34
+ else
35
+ echo "Successfully acquire /api, passing"
36
+ fi
37
+ fi
src/config.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { get_runtime } from "./util.js"
2
+
3
+ let OVERSEAS = globalThis?.Deno?.env?.get("OVERSEAS") || globalThis?.process?.env?.OVERSEAS
4
+ const runtime = get_runtime()
5
+
6
+ if (['cloudflare', 'vercel'].includes(runtime)) OVERSEAS = true
7
+
8
+ const PORT = globalThis?.Deno?.env?.get("PORT") || globalThis?.process?.env?.PORT || 3000
9
+
10
+ OVERSEAS = Boolean(OVERSEAS)
11
+
12
+ export default {
13
+ OVERSEAS,
14
+ PORT,
15
+ }
src/example.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+ "tencent": {
3
+ "playlist": { "show": true, "value": "7326220405" },
4
+ "song": { "show": true, "value": "002Rnpvi058Qdm" },
5
+ "lrc": { "show": false, "value": "000i26Sh1ZyiNU" },
6
+ "pic": { "show": false, "value": "002Rnpvi058Qdm" },
7
+ "url": { "show": false, "value": "002Rnpvi058Qdm" },
8
+ },
9
+ "netease": {
10
+ "playlist": { "show": true, "value": "6907557348" },
11
+ "song": { "show": true, "value": "473403185" },
12
+ "artist": { "show": true, "value": "12441107" },
13
+ "lrc": { "show": false, "value": "2015217630" },
14
+ "url": { "show": false, "value": "473403185" },
15
+ "search": { "show": true, "value": "KN33S0XXX" },
16
+ },
17
+ // "ytmusic": {
18
+ // "playlist": { "show": true, "value": "RDCLAK5uy_l12ynH8dyLsBmE11ToAHLm9P04NS2i9ME" },
19
+ // "song": { "show": true, "value": "G3s98l2-GXg" }
20
+ // },
21
+ // "spotify": {
22
+ // "playlist": { "show": true, "value": "4D7JFKXy4daI9tUVJfGVFF" },
23
+ // "song": { "show": true, "value": "5HU2Ddr33JPv7ZVI77M7D5" }
24
+ // }
25
+ }
src/providers/index.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tencent from "./tencent/index.js"
2
+ import netease from './netease/index.js'
3
+ import ytmusic from './ytmusic/index.js'
4
+ import spotify from "./spotify/index.js"
5
+
6
+ class Providers {
7
+
8
+ constructor() {
9
+ this.providers = {}
10
+
11
+ tencent.register(this)
12
+ netease.register(this)
13
+ ytmusic.register(this)
14
+ spotify.register(this)
15
+
16
+ }
17
+
18
+ register(provider_name, handle_obj) {
19
+ this.providers[provider_name] = handle_obj
20
+ }
21
+
22
+ get(provider_name) {
23
+ return this.providers[provider_name];
24
+ }
25
+
26
+ get_provider_list() {
27
+ return Object.keys(this.providers)
28
+ }
29
+ }
30
+
31
+ export default Providers
src/providers/netease/artist_songs.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request } from "./util.js"
2
+ import { map_song_list } from "./util.js"
3
+
4
+ export const get_artist_songs = async (id, cookie) => {
5
+ id = parseInt(id)
6
+ const data = {
7
+ id,
8
+ }
9
+
10
+ const res = await request('POST', `https://music.163.com/api/artist/top/song`, data, {
11
+ crypto: 'weapi',
12
+ cookie: {},
13
+ })
14
+
15
+ return map_song_list(res)
16
+
17
+ }
18
+
19
+ // const res = await get_artist_songs("12441107");
20
+ // console.log(res)
src/providers/netease/config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const net_ease_anonymous_token = "de91e1f8119d32e01cc73efcb82c0a30c9137e8d4f88dbf5e3d7bf3f28998f21add2bc8204eeee5e56c0bbb8743574b46ca2c10c35dc172199bef9bf4d60ecdeab066bb4dc737d1c3324751bcc9aaf44c3061cd18d77b7a0"
2
+
3
+ export {
4
+ net_ease_anonymous_token,
5
+ }
src/providers/netease/crypto.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // import crypto from 'https://esm.sh/crypto-browserify@3.12.0'
2
+ // import { Buffer } from 'https://esm.sh/buffer@6.0.3'
3
+
4
+ import crypto from 'crypto-browserify'
5
+ import { Buffer } from 'buffer/index.js'
6
+
7
+ // import crypto from 'node:crypto'
8
+ // import {Buffer} from 'node:buffer'
9
+
10
+ const iv = Buffer.from('0102030405060708')
11
+ const presetKey = Buffer.from('0CoJUm6Qyw8W8jud')
12
+ const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q')
13
+ const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
14
+ const publicKey =`-----BEGIN PUBLIC KEY-----
15
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ3
16
+ 7BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvakl
17
+ V8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44o
18
+ ncaTWz7OBGLbCiK45wIDAQAB
19
+ -----END PUBLIC KEY-----
20
+ `
21
+
22
+ const eapiKey = 'e82ckenh8dichen8'
23
+
24
+ const aesEncrypt = (buffer, mode, key, iv) => {
25
+ const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv)
26
+ return Buffer.concat([cipher.update(buffer), cipher.final()])
27
+ }
28
+
29
+ const rsaEncrypt = (buffer, key) => {
30
+ buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
31
+ return crypto.publicEncrypt(
32
+ { key: key, padding: crypto.constants.RSA_NO_PADDING },
33
+ buffer,
34
+ )
35
+ }
36
+
37
+ const weapi = (object) => {
38
+ const text = JSON.stringify(object)
39
+ const secretKey = crypto
40
+ .randomBytes(16)
41
+ .map((n) => base62.charAt(n % 62).charCodeAt())
42
+ return {
43
+ params: aesEncrypt(
44
+ Buffer.from(
45
+ aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64'),
46
+ ),
47
+ 'cbc',
48
+ secretKey,
49
+ iv,
50
+ ).toString('base64'),
51
+ encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
52
+ }
53
+ }
54
+
55
+ const linuxapi = (object) => {
56
+ const text = JSON.stringify(object)
57
+ return {
58
+ eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '')
59
+ .toString('hex')
60
+ .toUpperCase(),
61
+ }
62
+ }
63
+
64
+ const eapi = (url, object) => {
65
+ const text = typeof object === 'object' ? JSON.stringify(object) : object
66
+ const message = `nobody${url}use${text}md5forencrypt`
67
+ const digest = crypto.createHash('md5').update(message).digest('hex')
68
+ const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
69
+ return {
70
+ params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '')
71
+ .toString('hex')
72
+ .toUpperCase(),
73
+ }
74
+ }
75
+
76
+ const decrypt = (cipherBuffer) => {
77
+ const decipher = crypto.createDecipheriv('aes-128-ecb', eapiKey, '')
78
+ return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
79
+ }
80
+
81
+ export default { weapi, linuxapi, eapi, decrypt }
src/providers/netease/index.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { get_playlist } from "./playlist.js"
2
+ import { get_song_url, get_song_info } from "./song.js"
3
+ import { get_lyric } from "./lyric.js"
4
+ import { get_artist_songs } from "./artist_songs.js"
5
+ import { get_search_songs } from "./search.js"
6
+
7
+ const support_type = ['url', 'lrc', 'song', 'playlist', 'artist', 'search', 'pic']
8
+
9
+ const handle = async (type, id, cookie = '') => {
10
+ let result;
11
+ switch (type) {
12
+ case 'lrc':
13
+ result = await get_lyric(id)
14
+ break
15
+ case 'url':
16
+ result = await get_song_url(id)
17
+ break
18
+ case 'pic':
19
+ result = (await get_song_info(id))[0].pic
20
+ break
21
+ case 'song':
22
+ result = await get_song_info(id)
23
+ break
24
+ case 'playlist':
25
+ result = await get_playlist(id)
26
+ break
27
+ case 'artist':
28
+ result = await get_artist_songs(id)
29
+ break
30
+ case 'search':
31
+ result = await get_search_songs(id)
32
+ break
33
+ default:
34
+ return -1;
35
+ }
36
+ return result
37
+ }
38
+
39
+ export default {
40
+ register: (ctx) => {
41
+ ctx.register('netease', { handle, support_type })
42
+ }
43
+ }
src/providers/netease/lyric.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request } from "./util.js"
2
+
3
+ export const get_lyric = async (id, cookie) => {
4
+ // query.cookie.os = 'ios'
5
+
6
+ const data = {
7
+ id: id,
8
+ tv: -1,
9
+ lv: -1,
10
+ rv: -1,
11
+ kv: -1,
12
+ }
13
+ const res = await request(
14
+ 'POST',
15
+ `https://music.163.com/api/song/lyric?_nmclfl=1`,
16
+ data,
17
+ {
18
+ crypto: 'api',
19
+ },
20
+ )
21
+
22
+ return {
23
+ lyric: res.lrc?.lyric || '',
24
+ tlyric: res.tlyric?.lyric || ''
25
+ }
26
+ }
27
+
28
+ // const res = await get_lyric('2015217630')
29
+ // console.log(res)
src/providers/netease/playlist.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request } from './util.js'
2
+ import { map_song_list } from "./util.js"
3
+
4
+ export const get_playlist = async (id, cookie = '') => {
5
+ const data = {
6
+ id,
7
+ n: 100000,
8
+ s: 8,
9
+ }
10
+ //不放在data里面避免请求带上无用的数据
11
+ let limit = 200 || Infinity
12
+ let offset = 0 || 0
13
+
14
+ let res = await request('POST', `https://music.163.com/api/v6/playlist/detail`, data, { crypto: 'api', })
15
+
16
+ let trackIds = res.playlist.trackIds
17
+
18
+ let idsData = {
19
+ c:
20
+ '[' +
21
+ trackIds
22
+ .slice(offset, offset + limit)
23
+ .map((item) => '{"id":' + item.id + '}')
24
+ .join(',') +
25
+ ']',
26
+ }
27
+
28
+ res = await request(
29
+ 'POST',
30
+ `https://music.163.com/api/v3/song/detail`,
31
+ idsData,
32
+ { crypto: 'weapi' }
33
+ )
34
+
35
+ res = map_song_list(res)
36
+
37
+ return res
38
+
39
+ }
40
+
41
+ // const res = await get_playlist(2787254569)
42
+ // console.log(res)
43
+
src/providers/netease/search.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request } from "./util.js"
2
+ import { map_song_list } from "./util.js"
3
+
4
+ export const get_search_songs = async (id, cookie) => {
5
+
6
+ const data = {
7
+ s: id,
8
+ type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
9
+ limit: 30,
10
+ offset: 0,
11
+ total: true,
12
+ }
13
+
14
+ const res = await request('POST', `https://interface.music.163.com/eapi/cloudsearch`, data, {
15
+ crypto: 'eapi',
16
+ cookie: {},
17
+ url: '/api/cloudsearch/pc'
18
+ })
19
+
20
+ return map_song_list(res.result)
21
+
22
+ }
23
+
24
+ // const res = await get_search_songs("KN33S0XXX");
25
+ // console.log(res)
src/providers/netease/song.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request } from "./util.js"
2
+ import { map_song_list } from "./util.js"
3
+
4
+ export const get_song_url = async (id, cookie = '') => {
5
+
6
+ const data = {
7
+ ids: '[' + id + ']',
8
+ level: 'standard',
9
+ encodeType: 'flac',
10
+ }
11
+
12
+ let res = {}
13
+
14
+ try {
15
+ res = await request(
16
+ 'POST',
17
+ `https://interface.music.163.com/eapi/song/enhance/player/url/v1`,
18
+ data,
19
+ {
20
+ crypto: 'eapi',
21
+ url: '/api/song/enhance/player/url/v1',
22
+ cookie: {}
23
+ },
24
+ )
25
+ } catch (e) {
26
+ console.error(e)
27
+ }
28
+
29
+ const url = res.data && res.data[0]?.url?.replace('http://', 'https://')
30
+ return url || `https://music.163.com/song/media/outer/url?id=${id}.mp3`
31
+
32
+ }
33
+
34
+ export const get_song_info = async (id, cookie = '') => {
35
+ const ids = [id]
36
+ const data = {
37
+ c: '[' + ids.map((id) => '{"id":' + id + '}').join(',') + ']',
38
+ }
39
+ let res = await request('POST', `https://music.163.com/api/v3/song/detail`, data, {
40
+ crypto: 'weapi',
41
+ })
42
+
43
+ // console.log(res)
44
+
45
+ if (!res.songs) {
46
+ throw res
47
+ }
48
+
49
+ res = map_song_list(res)
50
+ return res
51
+ }
52
+
53
+
54
+ // const res = await get_song_info('1874976923');
55
+ // const res = await get_song_url('1874976923');
56
+ // console.log(res)
src/providers/netease/util.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import encrypt from './crypto.js'
2
+ import { net_ease_anonymous_token } from './config.js'
3
+ import { customAlphabet } from 'nanoid/non-secure'
4
+
5
+ const nanoid = customAlphabet('1234567890abcdef', 32)
6
+
7
+ const chooseUserAgent = (ua = false) => {
8
+ const userAgentList = {
9
+ mobile: [
10
+ // iOS 13.5.1 14.0 beta with safari
11
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1',
12
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.',
13
+ // iOS with qq micromsg
14
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML like Gecko) Mobile/14A456 QQ/6.5.7.408 V1_IPH_SQ_6.5.7_1_APP_A Pixel/750 Core/UIWebView NetType/4G Mem/103',
15
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.15(0x17000f27) NetType/WIFI Language/zh',
16
+ // Android -> Huawei Xiaomi
17
+ 'Mozilla/5.0 (Linux; Android 9; PCT-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.64 HuaweiBrowser/10.0.3.311 Mobile Safari/537.36',
18
+ 'Mozilla/5.0 (Linux; U; Android 9; zh-cn; Redmi Note 8 Build/PKQ1.190616.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.141 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.5.22',
19
+ // Android + qq micromsg
20
+ 'Mozilla/5.0 (Linux; Android 10; YAL-AL00 Build/HUAWEIYAL-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.62 XWEB/2581 MMWEBSDK/200801 Mobile Safari/537.36 MMWEBID/3027 MicroMessenger/7.0.18.1740(0x27001235) Process/toolsmp WeChat/arm64 NetType/WIFI Language/zh_CN ABI/arm64',
21
+ 'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; BKK-AL10 Build/HONORBKK-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/10.6 Mobile Safari/537.36',
22
+ ],
23
+ pc: [
24
+ // macOS 10.15.6 Firefox / Chrome / Safari
25
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
26
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
27
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15',
28
+ // Windows 10 Firefox / Chrome / Edge
29
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
30
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
31
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586',
32
+ // Linux 就算了
33
+ ],
34
+ }
35
+ let realUserAgentList =
36
+ userAgentList[ua] || userAgentList.mobile.concat(userAgentList.pc)
37
+ return ['mobile', 'pc', false].indexOf(ua) > -1
38
+ ? realUserAgentList[Math.floor(Math.random() * realUserAgentList.length)]
39
+ : ua
40
+ }
41
+
42
+ const cnip = () => {
43
+ //国内城市IP段
44
+ const ips = '58.14.0.0,58.16.0.0,58.24.0.0,58.30.0.0,58.32.0.0,58.66.0.0,58.68.128.0,58.82.0.0,58.87.64.0,58.99.128.0,58.100.0.0,58.116.0.0,58.128.0.0,58.144.0.0,58.154.0.0,58.192.0.0,58.240.0.0,59.32.0.0,59.64.0.0,59.80.0.0,59.107.0.0,59.108.0.0,59.151.0.0,59.155.0.0,59.172.0.0,59.191.0.0,59.191.240.0,59.192.0.0,60.0.0.0,60.55.0.0,60.63.0.0,60.160.0.0,60.194.0.0,60.200.0.0,60.208.0.0,60.232.0.0,60.235.0.0,60.245.128.0,60.247.0.0,60.252.0.0,60.253.128.0,60.255.0.0,61.4.80.0,61.4.176.0,61.8.160.0,61.28.0.0,61.29.128.0,61.45.128.0,61.47.128.0,61.48.0.0,61.87.192.0,61.128.0.0,61.232.0.0,61.236.0.0,61.240.0.0,114.28.0.0,114.54.0.0,114.60.0.0,114.64.0.0,114.68.0.0,114.80.0.0,116.1.0.0,116.2.0.0,116.4.0.0,116.8.0.0,116.13.0.0,116.16.0.0,116.52.0.0,116.56.0.0,116.58.128.0,116.58.208.0,116.60.0.0,116.66.0.0,116.69.0.0,116.70.0.0,116.76.0.0,116.89.144.0,116.90.184.0,116.95.0.0,116.112.0.0,116.116.0.0,116.128.0.0,116.192.0.0,116.193.16.0,116.193.32.0,116.194.0.0,116.196.0.0,116.198.0.0,116.199.0.0,116.199.128.0,116.204.0.0,116.207.0.0,116.208.0.0,116.212.160.0,116.213.64.0,116.213.128.0,116.214.32.0,116.214.64.0,116.214.128.0,116.215.0.0,116.216.0.0,116.224.0.0,116.242.0.0,116.244.0.0,116.248.0.0,116.252.0.0,116.254.128.0,116.255.128.0,117.8.0.0,117.21.0.0,117.22.0.0,117.24.0.0,117.32.0.0,117.40.0.0,117.44.0.0,117.48.0.0,117.53.48.0,117.53.176.0,117.57.0.0,117.58.0.0,117.59.0.0,117.60.0.0,117.64.0.0,117.72.0.0,117.74.64.0,117.74.128.0,117.75.0.0,117.76.0.0,117.80.0.0,117.100.0.0,117.103.16.0,117.103.128.0,117.106.0.0,117.112.0.0,117.120.64.0,117.120.128.0,117.121.0.0,117.121.128.0,117.121.192.0,117.122.128.0,117.124.0.0,117.128.0.0,118.24.0.0,118.64.0.0,118.66.0.0,118.67.112.0,118.72.0.0,118.80.0.0,118.84.0.0,118.88.32.0,118.88.64.0,118.88.128.0,118.89.0.0,118.91.240.0,118.102.16.0,118.112.0.0,118.120.0.0,118.124.0.0,118.126.0.0,118.132.0.0,118.144.0.0,118.178.0.0,118.180.0.0,118.184.0.0,118.192.0.0,118.212.0.0,118.224.0.0,118.228.0.0,118.230.0.0,118.239.0.0,118.242.0.0,118.244.0.0,118.248.0.0,119.0.0.0,119.2.0.0,119.2.128.0,119.3.0.0,119.4.0.0,119.8.0.0,119.10.0.0,119.15.136.0,119.16.0.0,119.18.192.0,119.18.208.0,119.18.224.0,119.19.0.0,119.20.0.0,119.27.64.0,119.27.160.0,119.27.192.0,119.28.0.0,119.30.48.0,119.31.192.0,119.32.0.0,119.40.0.0,119.40.64.0,119.40.128.0,119.41.0.0,119.42.0.0,119.42.136.0,119.42.224.0,119.44.0.0,119.48.0.0,119.57.0.0,119.58.0.0,119.59.128.0,119.60.0.0,119.62.0.0,119.63.32.0,119.75.208.0,119.78.0.0,119.80.0.0,119.84.0.0,119.88.0.0,119.96.0.0,119.108.0.0,119.112.0.0,119.128.0.0,119.144.0.0,119.148.160.0,119.161.128.0,119.162.0.0,119.164.0.0,119.176.0.0,119.232.0.0,119.235.128.0,119.248.0.0,119.253.0.0,119.254.0.0,120.0.0.0,120.24.0.0,120.30.0.0,120.32.0.0,120.48.0.0,120.52.0.0,120.64.0.0,120.72.32.0,120.72.128.0,120.76.0.0,120.80.0.0,120.90.0.0,120.92.0.0,120.94.0.0,120.128.0.0,120.136.128.0,120.137.0.0,120.192.0.0,121.0.16.0,121.4.0.0,121.8.0.0,121.16.0.0,121.32.0.0,121.40.0.0,121.46.0.0,121.48.0.0,121.51.0.0,121.52.160.0,121.52.208.0,121.52.224.0,121.55.0.0,121.56.0.0,121.58.0.0,121.58.144.0,121.59.0.0,121.60.0.0,121.68.0.0,121.76.0.0,121.79.128.0,121.89.0.0,121.100.128.0,121.101.208.0,121.192.0.0,121.201.0.0,121.204.0.0,121.224.0.0,121.248.0.0,121.255.0.0,122.0.64.0,122.0.128.0,122.4.0.0,122.8.0.0,122.48.0.0,122.49.0.0,122.51.0.0,122.64.0.0,122.96.0.0,122.102.0.0,122.102.64.0,122.112.0.0,122.119.0.0,122.136.0.0,122.144.128.0,122.152.192.0,122.156.0.0,122.192.0.0,122.198.0.0,122.200.64.0,122.204.0.0,122.224.0.0,122.240.0.0,122.248.48.0,123.0.128.0,123.4.0.0,123.8.0.0,123.49.128.0,123.52.0.0,123.56.0.0,123.64.0.0,123.96.0.0,123.98.0.0,123.99.128.0,123.100.0.0,123.101.0.0,123.103.0.0,123.108.128.0,123.108.208.0,123.112.0.0,123.128.0.0,123.136.80.0,123.137.0.0,123.138.0.0,123.144.0.0,123.160.0.0,123.176.80.0,123.177.0.0,123.178.0.0,123.180.0.0,123.184.0.0,123.196.0.0,123.199.128.0,123.206.0.0,123.232.0.0,123.242.0.0,123.244.0.0,123.249.0.0,123.253.0.0,124.6.64.0,124.14.0.0,124.16.0.0,124.20.0.0,124.28.192.0,124.29.0.0,124.31.0.0,124.40.112.0,124.40.128.0,124.42.0.0,124.47.0.0,124.64.0.0,124.66.0.0,124.67.0.0,124.68.0.0,124.72.0.0,124.88.0.0,124.108.8.0,124.108.40.0,124.112.0.0,124.126.0.0,124.128.0.0,124.147.128.0,124.156.0.0,124.160.0.0,124.172.0.0,124.192.0.0,124.196.0.0,124.200.0.0,124.220.0.0,124.224.0.0,124.240.0.0,124.240.128.0,124.242.0.0,124.243.192.0,124.248.0.0,124.249.0.0,124.250.0.0,124.254.0.0,125.31.192.0,125.32.0.0,125.58.128.0,125.61.128.0,125.62.0.0,125.64.0.0,125.96.0.0,125.98.0.0,125.104.0.0,125.112.0.0,125.169.0.0,125.171.0.0,125.208.0.0,125.210.0.0,125.213.0.0,125.214.96.0,125.215.0.0,125.216.0.0,125.254.128.0,134.196.0.0,159.226.0.0,161.207.0.0,162.105.0.0,166.111.0.0,167.139.0.0,168.160.0.0,169.211.1.0,192.83.122.0,192.83.169.0,192.124.154.0,192.188.170.0,198.17.7.0,202.0.110.0,202.0.176.0,202.4.128.0,202.4.252.0,202.8.128.0,202.10.64.0,202.14.88.0,202.14.235.0,202.14.236.0,202.14.238.0,202.20.120.0,202.22.248.0,202.38.0.0,202.38.64.0,202.38.128.0,202.38.136.0,202.38.138.0,202.38.140.0,202.38.146.0,202.38.149.0,202.38.150.0,202.38.152.0,202.38.156.0,202.38.158.0,202.38.160.0,202.38.164.0,202.38.168.0,202.38.176.0,202.38.184.0,202.38.192.0,202.41.152.0,202.41.240.0,202.43.144.0,202.46.32.0,202.46.224.0,202.60.112.0,202.63.248.0,202.69.4.0,202.69.16.0,202.70.0.0,202.74.8.0,202.75.208.0,202.85.208.0,202.90.0.0,202.90.224.0,202.90.252.0,202.91.0.0,202.91.128.0,202.91.176.0,202.91.224.0,202.92.0.0,202.92.252.0,202.93.0.0,202.93.252.0,202.95.0.0,202.95.252.0,202.96.0.0,202.112.0.0,202.120.0.0,202.122.0.0,202.122.32.0,202.122.64.0,202.122.112.0,202.122.128.0,202.123.96.0,202.124.24.0,202.125.176.0,202.127.0.0,202.127.12.0,202.127.16.0,202.127.40.0,202.127.48.0,202.127.112.0,202.127.128.0,202.127.160.0,202.127.192.0,202.127.208.0,202.127.212.0,202.127.216.0,202.127.224.0,202.130.0.0,202.130.224.0,202.131.16.0,202.131.48.0,202.131.208.0,202.136.48.0,202.136.208.0,202.136.224.0,202.141.160.0,202.142.16.0,202.143.16.0,202.148.96.0,202.149.160.0,202.149.224.0,202.150.16.0,202.152.176.0,202.153.48.0,202.158.160.0,202.160.176.0,202.164.0.0,202.164.25.0,202.165.96.0,202.165.176.0,202.165.208.0,202.168.160.0,202.170.128.0,202.170.216.0,202.173.8.0,202.173.224.0,202.179.240.0,202.180.128.0,202.181.112.0,202.189.80.0,202.192.0.0,203.18.50.0,203.79.0.0,203.80.144.0,203.81.16.0,203.83.56.0,203.86.0.0,203.86.64.0,203.88.32.0,203.88.192.0,203.89.0.0,203.90.0.0,203.90.128.0,203.90.192.0,203.91.32.0,203.91.96.0,203.91.120.0,203.92.0.0,203.92.160.0,203.93.0.0,203.94.0.0,203.95.0.0,203.95.96.0,203.99.16.0,203.99.80.0,203.100.32.0,203.100.80.0,203.100.96.0,203.100.192.0,203.110.160.0,203.118.192.0,203.119.24.0,203.119.32.0,203.128.32.0,203.128.96.0,203.130.32.0,203.132.32.0,203.134.240.0,203.135.96.0,203.135.160.0,203.142.219.0,203.148.0.0,203.152.64.0,203.156.192.0,203.158.16.0,203.161.192.0,203.166.160.0,203.171.224.0,203.174.7.0,203.174.96.0,203.175.128.0,203.175.192.0,203.176.168.0,203.184.80.0,203.187.160.0,203.190.96.0,203.191.16.0,203.191.64.0,203.191.144.0,203.192.0.0,203.196.0.0,203.207.64.0,203.207.128.0,203.208.0.0,203.208.16.0,203.208.32.0,203.209.224.0,203.212.0.0,203.212.80.0,203.222.192.0,203.223.0.0,210.2.0.0,210.5.0.0,210.5.144.0,210.12.0.0,210.14.64.0,210.14.112.0,210.14.128.0,210.15.0.0,210.15.128.0,210.16.128.0,210.21.0.0,210.22.0.0,210.23.32.0,210.25.0.0,210.26.0.0,210.28.0.0,210.32.0.0,210.51.0.0,210.52.0.0,210.56.192.0,210.72.0.0,210.76.0.0,210.78.0.0,210.79.64.0,210.79.224.0,210.82.0.0,210.87.128.0,210.185.192.0,210.192.96.0,211.64.0.0,211.80.0.0,211.96.0.0,211.136.0.0,211.144.0.0,211.160.0.0,218.0.0.0,218.56.0.0,218.64.0.0,218.96.0.0,218.104.0.0,218.108.0.0,218.185.192.0,218.192.0.0,218.240.0.0,218.249.0.0,219.72.0.0,219.82.0.0,219.128.0.0,219.216.0.0,219.224.0.0,219.242.0.0,219.244.0.0,220.101.192.0,220.112.0.0,220.152.128.0,220.154.0.0,220.160.0.0,220.192.0.0,220.231.0.0,220.231.128.0,220.232.64.0,220.234.0.0,220.242.0.0,220.248.0.0,220.252.0.0,221.0.0.0,221.8.0.0,221.12.0.0,221.12.128.0,221.13.0.0,221.14.0.0,221.122.0.0,221.129.0.0,221.130.0.0,221.133.224.0,221.136.0.0,221.172.0.0,221.176.0.0,221.192.0.0,221.196.0.0,221.198.0.0,221.199.0.0,221.199.128.0,221.199.192.0,221.199.224.0,221.200.0.0,221.208.0.0,221.224.0.0,222.16.0.0,222.32.0.0,222.64.0.0,222.125.0.0,222.126.128.0,222.128.0.0,222.160.0.0,222.168.0.0,222.176.0.0,222.192.0.0,222.240.0.0,222.248.0.0';
45
+ const arr = ips.split(',');
46
+ const rnd = parseInt(Math.random() * ((arr.length - 1) - 0 + 1) + 0, 10)
47
+ const ip = arr[rnd];
48
+ return ip;
49
+ }
50
+
51
+
52
+ export const request = async (method, url, data = {}, options) => {
53
+
54
+ let headers = { 'User-Agent': chooseUserAgent(options.ua) }
55
+ if (method.toUpperCase() === 'POST')
56
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
57
+ if (url.includes('music.163.com'))
58
+ headers['Referer'] = 'https://music.163.com'
59
+
60
+ let ip = cnip() || ''
61
+ // console.log(ip)
62
+ if (ip) {
63
+ headers['X-Real-IP'] = ip
64
+ headers['X-Forwarded-For'] = ip
65
+ }
66
+ // headers['X-Real-IP'] = '118.88.88.88'
67
+ if (typeof options.cookie === "undefined")
68
+ options.cookie = {}
69
+
70
+ if (typeof options.cookie === 'object') {
71
+ options.cookie = {
72
+ ...options.cookie,
73
+ __remember_me: true,
74
+ // NMTID: nanoid(),
75
+ _ntes_nuid: nanoid(),
76
+ }
77
+ if (!options.cookie.MUSIC_U) {
78
+ // 游客
79
+ if (!options.cookie.MUSIC_A) {
80
+ options.cookie.MUSIC_A = net_ease_anonymous_token
81
+ }
82
+ }
83
+ headers['Cookie'] = Object.keys(options.cookie)
84
+ .map(
85
+ (key) =>
86
+ encodeURIComponent(key) +
87
+ '=' +
88
+ encodeURIComponent(options.cookie[key]),
89
+ )
90
+ .join('; ')
91
+ } else if (options.cookie) {
92
+ headers['Cookie'] = options.cookie
93
+ } else {
94
+ headers['Cookie'] = '__remember_me=true; NMTID=xxx'
95
+ }
96
+
97
+ // console.log(options.cookie, headers['Cookie'])
98
+ if (options.crypto === 'weapi') {
99
+ let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
100
+ data.csrf_token = csrfToken ? csrfToken[1] : ''
101
+ data = encrypt.weapi(data)
102
+ url = url.replace(/\w*api/, 'weapi')
103
+ } else if (options.crypto === 'linuxapi') {
104
+ data = encrypt.linuxapi({
105
+ method: method,
106
+ url: url.replace(/\w*api/, 'api'),
107
+ params: data,
108
+ })
109
+ headers['User-Agent'] =
110
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
111
+ url = 'https://music.163.com/api/linux/forward'
112
+ } else if (options.crypto === 'eapi') {
113
+ const cookie = options.cookie || {}
114
+ const csrfToken = cookie['__csrf'] || ''
115
+ const header = {
116
+ osver: cookie.osver, //系统版本
117
+ deviceId: cookie.deviceId, //encrypt.base64.encode(imei + '\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7')
118
+ appver: cookie.appver || '8.7.01', // app版本
119
+ versioncode: cookie.versioncode || '140', //版本号
120
+ mobilename: cookie.mobilename, //设备model
121
+ buildver: cookie.buildver || Date.now().toString().substr(0, 10),
122
+ resolution: cookie.resolution || '1920x1080', //设备分辨率
123
+ __csrf: csrfToken,
124
+ os: cookie.os || 'android',
125
+ channel: cookie.channel,
126
+ requestId: `${Date.now()}_${Math.floor(Math.random() * 1000)
127
+ .toString()
128
+ .padStart(4, '0')
129
+ } `,
130
+ }
131
+ if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
132
+ if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
133
+ headers['Cookie'] = Object.keys(header)
134
+ .map(
135
+ (key) =>
136
+ encodeURIComponent(key) + '=' + encodeURIComponent(header[key]),
137
+ )
138
+ .join('; ')
139
+ data.header = header
140
+ data = encrypt.eapi(options.url, data)
141
+ url = url.replace(/\w*api/, 'eapi')
142
+ }
143
+
144
+ let settings = {
145
+ method,
146
+ headers,
147
+ body: new URLSearchParams(data).toString()
148
+ }
149
+
150
+
151
+ if (options.crypto === 'eapi') {
152
+ settings = {
153
+ ...settings,
154
+ responseType: 'arraybuffer',
155
+ }
156
+ }
157
+
158
+
159
+ let res, count = 0
160
+ do {
161
+ res = await fetch(url, settings)
162
+ if (options.crypto === 'eapi') {
163
+ const _ = await res.arrayBuffer('utf-8')
164
+ const enc = new TextDecoder()
165
+ res = JSON.parse(enc.decode(_))
166
+ } else {
167
+ res = await res.json()
168
+ }
169
+ count++
170
+ if (count > 1) {
171
+ console.log(`Request ${count} times.`)
172
+ }
173
+ if (count > 5) {
174
+ console.error(`Max retries exceeded.`)
175
+ break
176
+ }
177
+ await new Promise(resolve => setTimeout(resolve, 100))
178
+ } while (res.code == -460) // { code: -460, message: '网络太拥挤,请稍候再试!' }
179
+
180
+ return res;
181
+
182
+ }
183
+
184
+ export const map_song_list = (song_list) => {
185
+ return song_list.songs.map(song => {
186
+ const artists = song.ar || song.artists
187
+ return {
188
+ title: song.name,
189
+ author: artists.reduce((i, v) => ((i ? i + " / " : i) + v.name), ''),
190
+ pic: song?.al?.picUrl || song.id,
191
+ url: song.id,
192
+ lrc: song.id
193
+ }
194
+ })
195
+ }
src/providers/spotify/config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ const SPOTIFY_API = globalThis?.Deno?.env?.get("SPOTIFY_API")
2
+ || globalThis?.process?.env?.SPOTIFY_API
3
+ || globalThis?.Deno?.env?.get("YT_API")
4
+ || globalThis?.process?.env?.YT_API
5
+
6
+ export {
7
+ SPOTIFY_API,
8
+ }
src/providers/spotify/index.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SPOTIFY_API } from "./config.js"
2
+ const support_type = ['song', 'playlist']
3
+
4
+ const handle = async (type, id, cookie = '') => {
5
+ let result
6
+ const query = `?server=spotify&type=${type}&id=${id}`
7
+ if (support_type.includes(type)) {
8
+ result = await fetch(SPOTIFY_API + query)
9
+ result = await result.json()
10
+ } else {
11
+ result = -1
12
+ }
13
+
14
+ return result
15
+ }
16
+
17
+ export default {
18
+ register: (ctx) => {
19
+ ctx.register('spotify', { handle, support_type })
20
+ }
21
+ }
src/providers/tencent/index.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { get_playlist } from "./playlist.js";
2
+ import { get_song_url, get_song_info, get_pic } from "./song.js";
3
+ import { get_lyric } from "./lyric.js"
4
+
5
+ const support_type = ['url', 'pic', 'lrc', 'song', 'playlist']
6
+
7
+ const handle = async (type, id, cookie = '') => {
8
+ let result;
9
+ switch (type) {
10
+ case 'lrc':
11
+ result = await get_lyric(id)
12
+ break
13
+ case 'pic':
14
+ result = await get_pic(id)
15
+ break
16
+ case 'url':
17
+ result = await get_song_url(id)
18
+ break
19
+ case 'song':
20
+ result = await get_song_info(id)
21
+ break
22
+ case 'playlist':
23
+ result = await get_playlist(id)
24
+ break
25
+ default:
26
+ return -1;
27
+ }
28
+ return result
29
+ }
30
+
31
+ export default {
32
+ register: (ctx) => {
33
+ ctx.register('tencent', { handle, support_type })
34
+ }
35
+ }
src/providers/tencent/lyric.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { changeUrlQuery } from "./util.js"
2
+
3
+ const get_lyric = async (songmid, cookie = '') => {
4
+ const data = {
5
+ songmid,
6
+ pcachetime: new Date().getTime(),
7
+ g_tk: 5381,
8
+ loginUin: 0,
9
+ hostUin: 0,
10
+ inCharset: 'utf8',
11
+ outCharset: 'utf-8',
12
+ notice: 0,
13
+ platform: 'yqq',
14
+ needNewCode: 0,
15
+ format: "json"
16
+ }
17
+
18
+
19
+ const headers = {
20
+ Referer: 'https://y.qq.com',
21
+ }
22
+
23
+ const url = changeUrlQuery(data, 'http://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg')
24
+
25
+ let result = await fetch(url, { headers });
26
+
27
+ result = await result.json()
28
+
29
+ result.lyric = decodeURIComponent(escape(atob(result.lyric || '')));
30
+ result.trans = decodeURIComponent(escape(atob(result.trans || '')));
31
+
32
+ const res = { lyric: result.lyric, tlyric: result.trans }
33
+ return res;
34
+ }
35
+
36
+
37
+ // const res = await get_lyric('000i26Sh1ZyiNU')
38
+ // console.log(res)
39
+
40
+ export { get_lyric }
src/providers/tencent/playlist.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from "../../config.js"
2
+ import { get_song_url } from "./song.js"
3
+ import { changeUrlQuery } from "./util.js"
4
+
5
+ const get_playlist = async (id, cookie = '') => {
6
+ const data = {
7
+ type: 1,
8
+ utf8: 1,
9
+ disstid: id,
10
+ loginUin: 0,
11
+ format: 'json'
12
+ }
13
+
14
+
15
+ const headers = {
16
+ Referer: 'https://y.qq.com/n/yqq/playlist',
17
+ }
18
+
19
+ const url = changeUrlQuery(data, 'http://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg')
20
+
21
+ let result = await fetch(url, { headers });
22
+
23
+ result = await result.json()
24
+ result = result.cdlist[0].songlist
25
+
26
+ let jsonp
27
+ if (config.OVERSEAS) {
28
+ const ids = result.map(song => song.songmid)
29
+ jsonp = await get_song_url(ids.join(','))
30
+ }
31
+ const res = await Promise.all(result.map(async song => {
32
+ let song_info = {
33
+ author: song.singer.reduce((i, v) => ((i ? i + " / " : i) + v.name), ''),
34
+ title: song.songname,
35
+ pic: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${song.albummid}.jpg`,
36
+ url: config.OVERSEAS ? '' : song.songmid,
37
+ lrc: song.songmid,
38
+ songmid: song.songmid,
39
+ }
40
+ return song_info
41
+ }));
42
+
43
+ if (config.OVERSEAS) res[0].url = jsonp
44
+ return res;
45
+ }
46
+
47
+
48
+ // const res = await get_playlist('7326220405')
49
+ // console.log(res)
50
+
51
+ export { get_playlist }
src/providers/tencent/song.js ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { changeUrlQuery } from "./util.js"
2
+ import config from "../../config.js"
3
+
4
+ export const get_song_url = async (id, cookie = '') => {
5
+
6
+ id = id.split(',')
7
+ let uin = ''
8
+ let qqmusic_key = ''
9
+ const typeObj = {
10
+ s: 'M500',
11
+ e: '.mp3',
12
+ }
13
+
14
+ const file = id.map(e => `${typeObj.s}${e}${e}${typeObj.e}`)
15
+ const guid = (Math.random() * 10000000).toFixed(0);
16
+
17
+ let purl = '';
18
+
19
+ let data = {
20
+ req_0: {
21
+ module: 'vkey.GetVkeyServer',
22
+ method: 'CgiGetVkey',
23
+ param: {
24
+ // filename: file,
25
+ guid: guid,
26
+ songmid: id,
27
+ songtype: [0],
28
+ uin: uin,
29
+ loginflag: 1,
30
+ platform: '20',
31
+ },
32
+ },
33
+ comm: {
34
+ uin: uin,
35
+ format: 'json',
36
+ ct: 19,
37
+ cv: 0,
38
+ authst: qqmusic_key,
39
+ },
40
+ }
41
+
42
+ let params = {
43
+ '-': 'getplaysongvkey',
44
+ g_tk: 5381,
45
+ loginUin: uin,
46
+ hostUin: 0,
47
+ format: 'json',
48
+ inCharset: 'utf8',
49
+ outCharset: 'utf-8¬ice=0',
50
+ platform: 'yqq.json',
51
+ needNewCode: 0,
52
+ data: JSON.stringify(data),
53
+ }
54
+
55
+
56
+ if (config.OVERSEAS || id.length > 1) {
57
+ params.format = 'jsonp'
58
+ const callback_function_name = 'callback'
59
+ const callback_name = "callback"
60
+ const parse_function = "qq_get_url_from_json"
61
+ const url = changeUrlQuery(params, 'https://u.y.qq.com/cgi-bin/musicu.fcg')
62
+ return "@" + parse_function + '@' + callback_name + '@' + callback_function_name + '@' + url
63
+ }
64
+
65
+
66
+ const url = changeUrlQuery(params, 'https://u.y.qq.com/cgi-bin/musicu.fcg')
67
+
68
+ let result = await fetch(url);
69
+
70
+ result = await result.json()
71
+ // console.log(result)
72
+ if (result.req_0 && result.req_0.data && result.req_0.data.midurlinfo) {
73
+ purl = result.req_0.data.midurlinfo[0].purl;
74
+ }
75
+
76
+ const domain =
77
+ result.req_0.data.sip.find(i => !i.startsWith('http://ws')) ||
78
+ result.req_0.data.sip[0];
79
+
80
+ const res = `${domain}${purl}`.replace('http://', 'https://')
81
+ // console.log(res);
82
+ return res;
83
+
84
+ }
85
+
86
+ export const get_song_info = async (id, cookie = '') => {
87
+ const data = {
88
+ data: JSON.stringify({
89
+ songinfo: {
90
+ method: 'get_song_detail_yqq',
91
+ module: 'music.pf_song_detail_svr',
92
+ param: {
93
+ song_mid: id,
94
+ },
95
+ },
96
+ }),
97
+ };
98
+
99
+ const url = changeUrlQuery(data, 'http://u.y.qq.com/cgi-bin/musicu.fcg');
100
+
101
+ let result = await fetch(url);
102
+
103
+ result = await result.json()
104
+
105
+ result = result.songinfo.data
106
+
107
+ let song_info = {
108
+ author: result.track_info.singer.reduce((i, v) => ((i ? i + " / " : i) + v.name), ''),
109
+ title: result.track_info.name,
110
+ pic: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${result.track_info.album.mid}.jpg`,
111
+ url: config.OVERSEAS ? await get_song_url(id) : id,
112
+ lrc: id,
113
+ songmid: id,
114
+ }
115
+ // console.log(song_info)
116
+ return [song_info]
117
+ }
118
+
119
+ export const get_pic = async (id, cookie = '') => {
120
+ const info = await get_song_info(id, cookie)
121
+ return info[0].pic
122
+ }
123
+
124
+ // const res = await get_song_url('002Rnpvi058Qdm');
125
+ // console.log(res)
126
+
127
+ // const res = await get_song_url('002Rnpvi058Qdm,000i26Sh1ZyiNU');
128
+ // console.log(res)
129
+
130
+ // const res = await get_song_info('002Rnpvi058Qdm');
131
+ // console.log(res)
src/providers/tencent/util.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function getQueryFromUrl(key, search) {
2
+ try {
3
+ const sArr = search.split('?');
4
+ let s = '';
5
+ if (sArr.length > 1) {
6
+ s = sArr[1];
7
+ } else {
8
+ return key ? undefined : {};
9
+ }
10
+ const querys = s.split('&');
11
+ const result = {};
12
+ querys.forEach((item) => {
13
+ const temp = item.split('=');
14
+ result[temp[0]] = decodeURIComponent(temp[1]);
15
+ });
16
+ return key ? result[key] : result;
17
+ } catch (err) {
18
+ // 除去search为空等异常
19
+ return key ? '' : {};
20
+ }
21
+ }
22
+
23
+ function changeUrlQuery(obj, baseUrl) {
24
+ const query = getQueryFromUrl(null, baseUrl);
25
+ let url = baseUrl.split('?')[0];
26
+
27
+ const newQuery = { ...query, ...obj };
28
+ let queryArr = [];
29
+ Object.keys(newQuery).forEach((key) => {
30
+ if (newQuery[key] !== undefined && newQuery[key] !== '') {
31
+ queryArr.push(`${key}=${encodeURIComponent(newQuery[key])}`);
32
+ }
33
+ });
34
+ return `${url}?${queryArr.join('&')}`.replace(/\?$/, '');
35
+ }
36
+
37
+ export { changeUrlQuery }
src/providers/ytmusic/config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const YT_API = globalThis?.Deno?.env?.get("YT_API") || globalThis?.process?.env?.YT_API
2
+
3
+ export {
4
+ YT_API,
5
+ }
src/providers/ytmusic/index.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { YT_API } from "./config.js"
2
+ const support_type = ['song', 'playlist']
3
+
4
+ const handle = async (type, id, cookie = '') => {
5
+ let result
6
+ const query = `?server=ytmusic&type=${type}&id=${id}`
7
+ if (support_type.includes(type)) {
8
+ result = await fetch(YT_API + query)
9
+ result = await result.json()
10
+ } else {
11
+ result = -1
12
+ }
13
+
14
+ return result
15
+ }
16
+
17
+ export default {
18
+ register: (ctx) => {
19
+ ctx.register('ytmusic', { handle, support_type })
20
+ }
21
+ }
src/service/api.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Providers from "../providers/index.js"
2
+ import { format as lyricFormat, get_url } from "../util.js"
3
+
4
+ export default async (ctx) => {
5
+
6
+ const p = new Providers()
7
+
8
+ const query = ctx.req.query()
9
+ const server = query.server || 'tencent'
10
+ const type = query.type || 'playlist'
11
+ const id = query.id || '7326220405'
12
+
13
+ if (!p.get_provider_list().includes(server) || !p.get(server).support_type.includes(type)) {
14
+ ctx.status(400)
15
+ return ctx.json({ status: 400, message: 'server 参数不合法', param: { server, type, id } })
16
+ }
17
+
18
+ let data = await p.get(server).handle(type, id)
19
+
20
+ if (type === 'url') {
21
+ let url = data
22
+
23
+ if (!url) {
24
+ ctx.status(403)
25
+ return ctx.json({ error: 'no url' })
26
+ }
27
+ if (url.startsWith('@'))
28
+ return ctx.text(url)
29
+
30
+ return ctx.redirect(url)
31
+ }
32
+
33
+ if (type === 'pic') {
34
+ return ctx.redirect(data)
35
+ }
36
+
37
+ if (type === 'lrc') {
38
+ return ctx.text(lyricFormat(data.lyric, data.tlyric || ''))
39
+ }
40
+
41
+
42
+ // json 类型数据填充api
43
+ return ctx.json(data.map(x => {
44
+ for (let i of ['url', 'pic', 'lrc']) {
45
+ const _ = String(x[i])
46
+ // 正常对象_均为id,以下例外不用填充:1.@开头/size为0=>qq音乐jsonp 2.已存在完整链接
47
+ if (!_.startsWith('@') && !_.startsWith('http') && _.length > 0) {
48
+ x[i] = `${get_url(ctx)}?server=${server}&type=${i}&id=${_}`
49
+ }
50
+ }
51
+ return x
52
+ }))
53
+ }
src/template.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import example from "./example.js"
2
+
3
+ let html = `
4
+ <!DOCTYPE html>
5
+ <html>
6
+
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <title>测试页面</title>
10
+ <link rel="stylesheet" href="https://unpkg.com/aplayer/dist/APlayer.min.css">
11
+ </head>
12
+
13
+ <body>
14
+ <script src="https://unpkg.com/aplayer/dist/APlayer.min.js"></script>
15
+ <script>
16
+ var meting_api = 'api?server=:server&type=:type&id=:id&auth=:auth&r=:r';
17
+ </script>
18
+ <script src="https://unpkg.com/@xizeyoupan/meting@latest/dist/Meting.min.js"></script>
19
+ `
20
+
21
+ Object.keys(example).map(provider => {
22
+ Object.keys(example[provider]).map(type => {
23
+ if (!example[provider][type].show) return
24
+
25
+ html += `
26
+ <div>
27
+ <p>${provider} ${type}</p>
28
+ <meting-js server="${provider}" type="${type}" id="${example[provider][type].value}" list-folded=true />
29
+ </div>
30
+ <br/>
31
+ `
32
+ })
33
+ })
34
+
35
+ html += `
36
+ </body>
37
+
38
+ </html>
39
+ `
40
+
41
+ export const handler = (c) => {
42
+ return c.html(html)
43
+ }
src/util.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function format(lyric, tlyric) {
2
+ const lyricArray = trimLyric(lyric)
3
+ const tlyricArray = trimLyric(tlyric)
4
+ if (tlyricArray.length === 0) {
5
+ return lyric
6
+ }
7
+ const result = []
8
+ for (let i = 0, j = 0; i < lyricArray.length && j < tlyricArray.length; i += 1) {
9
+ const time = lyricArray[i].time
10
+ let text = lyricArray[i].text
11
+ while (time > tlyricArray[j].time && j + 1 < tlyricArray.length) {
12
+ j += 1
13
+ }
14
+ if (time === tlyricArray[j].time && tlyricArray[j].text.length) {
15
+ text = `${text} (${tlyricArray[j].text})`
16
+ }
17
+ result.push({
18
+ time,
19
+ text
20
+ })
21
+ }
22
+ return result
23
+ .map(x => {
24
+ const minus = Math.floor(x.time / 60000).toString().padStart(2, '0')
25
+ const second = Math.floor((x.time % 60000) / 1000).toString().padStart(2, '0')
26
+ const millisecond = Math.floor((x.time % 1000)).toString().padStart(3, '0')
27
+ return `[${minus}:${second}.${millisecond}]${x.text}`
28
+ })
29
+ .join('\n')
30
+ }
31
+
32
+ const trimLyric = (lyric) => {
33
+ const result = []
34
+ const lines = lyric.split('\n')
35
+ for (const line of lines) {
36
+ const match = line.match(/^\[(\d{2}):(\d{2}\.\d*)\](.*)$/)
37
+ if (match) {
38
+ result.push({
39
+ time: parseInt(parseInt(match[1], 10) * 60 * 1000 + parseFloat(match[2]) * 1000),
40
+ text: match[3]
41
+ })
42
+ }
43
+ }
44
+ return result.sort((a, b) => a.time - b.time)
45
+ }
46
+
47
+ export const getPathFromURL = (url, strict = true) => {
48
+ const queryIndex = url.indexOf("?");
49
+ const result = url.substring(url.indexOf("/", 8), queryIndex === -1 ? url.length : queryIndex);
50
+ if (strict === false && result.endsWith("/")) {
51
+ return result.slice(0, -1);
52
+ }
53
+ return result;
54
+ };
55
+
56
+ export const get_runtime = () => {
57
+
58
+ if (globalThis?.process?.env?.RUNTIME) {
59
+ return globalThis?.process?.env?.RUNTIME
60
+ }
61
+
62
+ if (globalThis?.Deno !== undefined) {
63
+ return 'deno'
64
+ }
65
+
66
+ if (globalThis?.Bun !== undefined) {
67
+ return 'bun'
68
+ }
69
+
70
+ if (typeof globalThis?.WebSocketPair === 'function') {
71
+ return 'cloudflare'
72
+ }
73
+
74
+ if (globalThis?.fastly !== undefined) {
75
+ return 'fastly'
76
+ }
77
+
78
+ if (typeof globalThis?.EdgeRuntime === 'string') {
79
+ return 'vercel'
80
+ }
81
+
82
+ if (globalThis?.process?.release?.name === 'node') {
83
+ return 'node'
84
+ }
85
+
86
+ if (globalThis?.__lagon__ !== undefined) {
87
+ return 'lagon'
88
+ }
89
+
90
+ return 'other'
91
+ }
92
+
93
+ export const get_url = (ctx) => {
94
+ const runtime = get_runtime()
95
+ const perfix = ctx.req.header('X-Forwarded-Host') || ctx.req.header('X-Forwarded-Url')
96
+ let req_url = perfix ? perfix + getPathFromURL(ctx.req.url.split('?')[0]) : ctx.req.url.split('?')[0]
97
+ if (!req_url.startsWith('http')) req_url = 'http://' + req_url
98
+ if (runtime === 'vercel') req_url = req_url.replace('http://', 'https://')
99
+ return req_url
100
+ }
test/providers.test.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Providers from "../src/providers/index.js"
2
+ import examples from "../src/example.js"
3
+ import api from '../src/service/api.js'
4
+ import { Hono } from 'hono'
5
+ import { cors } from 'hono/cors'
6
+ import { test, expect } from 'vitest'
7
+
8
+ const app = new Hono()
9
+ app.use('*', cors())
10
+ app.get('/api', api)
11
+
12
+ // test('examples match providers', () => {
13
+ // const p = new Providers()
14
+ // const provider_list = p.get_provider_list()
15
+ // expect(new Set(provider_list)).toEqual(new Set(Object.keys(examples)))
16
+ // })
17
+
18
+ test('test provider support_type', () => {
19
+ const p = new Providers()
20
+ Object.keys(examples).map(provider_name => {
21
+ const provider = p.get(provider_name)
22
+ Object.keys(examples[provider_name]).map(type => {
23
+ expect(provider.support_type).toContain(type)
24
+ })
25
+ })
26
+ })
27
+
28
+ const YT_API = globalThis?.Deno?.env?.get("YT_API") || globalThis?.process?.env?.YT_API
29
+
30
+ test('test api', async () => {
31
+ for (const provider_name in examples) {
32
+ if (["ytmusic", "spotify"].includes(provider_name) && !YT_API) {
33
+ console.log("external api not found, skipping...")
34
+ continue
35
+ }
36
+ for (const type in examples[provider_name]) {
37
+
38
+ const url = `http://localhost:3000/api?server=${provider_name}&type=${type}&id=${examples[provider_name][type].value}`
39
+ let res, count = 0
40
+ while (count < 5) {
41
+ res = await app.request(url)
42
+ console.log("testing " + url)
43
+ console.log(res.status)
44
+ if (200 <= res.status && res.status < 400) {
45
+ break
46
+ } else {
47
+ count++
48
+ console.log("retrying " + count)
49
+ await new Promise(resolve => setTimeout(resolve, 1000))
50
+ }
51
+ }
52
+
53
+ expect(res).toBeDefined()
54
+ expect(res.status).toBeGreaterThanOrEqual(200)
55
+ expect(res.status).toBeLessThan(400)
56
+ }
57
+
58
+ }
59
+ }, 10 * 60 * 1000)
vercel.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "env": {
3
+ "RUNTIME": "vercel"
4
+ },
5
+ "rewrites": [
6
+ {
7
+ "source": "/(.*)",
8
+ "destination": "/api"
9
+ }
10
+ ]
11
+ }