Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse files- .dockerignore +4 -0
- .gitattributes +2 -0
- .github/workflows/publish.yml +146 -0
- .gitignore +4 -0
- .npmrc +1 -0
- Dockerfile +24 -0
- README.md +11 -6
- api/index.js +98 -0
- app.js +47 -0
- assets/1.png +3 -0
- assets/2.png +0 -0
- assets/3.png +0 -0
- assets/4.png +0 -0
- assets/5.png +0 -0
- assets/6.png +0 -0
- assets/7.png +0 -0
- assets/test.png +3 -0
- deno.js +6 -0
- esbuild.config.js +69 -0
- node.js +9 -0
- package-lock.json +0 -0
- package.json +36 -0
- scripts/docker/test-docker.sh +37 -0
- src/config.js +15 -0
- src/example.js +25 -0
- src/providers/index.js +31 -0
- src/providers/netease/artist_songs.js +20 -0
- src/providers/netease/config.js +5 -0
- src/providers/netease/crypto.js +81 -0
- src/providers/netease/index.js +43 -0
- src/providers/netease/lyric.js +29 -0
- src/providers/netease/playlist.js +43 -0
- src/providers/netease/search.js +25 -0
- src/providers/netease/song.js +56 -0
- src/providers/netease/util.js +195 -0
- src/providers/spotify/config.js +8 -0
- src/providers/spotify/index.js +21 -0
- src/providers/tencent/index.js +35 -0
- src/providers/tencent/lyric.js +40 -0
- src/providers/tencent/playlist.js +51 -0
- src/providers/tencent/song.js +131 -0
- src/providers/tencent/util.js +37 -0
- src/providers/ytmusic/config.js +5 -0
- src/providers/ytmusic/index.js +21 -0
- src/service/api.js +53 -0
- src/template.js +43 -0
- src/util.js +100 -0
- test/providers.test.js +59 -0
- vercel.json +11 -0
.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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "Meting-API"
|
| 3 |
+
emoji: "🚀"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 3000
|
| 8 |
---
|
| 9 |
|
| 10 |
+
### 🚀 一键部署
|
| 11 |
+
[](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
|
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
|
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 |
+
}
|