refactor: version 2.0 (React)

This commit is contained in:
qier222 2022-03-13 14:40:38 +08:00
parent 7dad7d810a
commit 950f72d4e8
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
356 changed files with 7901 additions and 29547 deletions

View File

@ -1,16 +0,0 @@
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.github
.gitignore
README.md
LICENSE
.vscode
dist
dist_electron
build
images
script

View File

@ -1,8 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View File

@ -0,0 +1,46 @@
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
module.exports = {
appId: 'yesplaymusic',
productName: 'YesPlayMusic',
copyright: 'Copyright © 2022 ${author}',
asar: true,
directories: {
output: 'release/${version}',
buildResources: 'build',
},
files: ['dist'],
win: {
target: [
{
target: 'nsis',
arch: ['x64'],
},
{
target: 'nsis',
arch: ['arm64'],
},
{
target: 'nsis',
arch: ['ia32'],
},
],
artifactName: '${productName}-${version}-Setup.${ext}',
},
nsis: {
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
deleteAppDataOnUninstall: true,
},
mac: {
target: ['dmg'],
artifactName: '${productName}-${version}-Installer.${ext}',
},
linux: {
target: ['AppImage'],
artifactName: '${productName}-${version}-Installer.${ext}',
},
}

3
.env Normal file
View File

@ -0,0 +1,3 @@
ELECTRON_WEB_SERVER_PORT = 42710
ELECTRON_DEV_NETEASE_API_PORT = 3000
VITE_APP_NETEASE_API_URL = /netease

View File

@ -1,7 +0,0 @@
VUE_APP_NETEASE_API_URL=/api
VUE_APP_ELECTRON_API_URL=/api
VUE_APP_ELECTRON_API_URL_DEV=http://127.0.0.1:10754
VUE_APP_LASTFM_API_KEY=09c55292403d961aa517ff7f5e8a3d9c
VUE_APP_LASTFM_API_SHARED_SECRET=307c9fda32b3904e53654baff215cb67
DEV_SERVER_PORT=20201

28
.eslintrc.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
},
}

View File

@ -1,21 +0,0 @@
---
name: 反馈问题或请求新功能
about: bug & feature
title: ''
labels: ''
assignees: ''
---
# 尽量每个 issue 只提一个 bug 或新功能
### 提新 issue 前请确认 👉
- 没人提过这个 issue[这里看所有 issue](https://github.com/qier222/YesPlayMusic/issues)
- 项目的 Todo 里没有与你 issue 相关的内容([这里看 Todo](https://github.com/qier222/YesPlayMusic/projects/1)
### 反馈 bug 需要的信息
- 用的是网页版还是客户端
- 浏览器名称或电脑操作系统
- 控制台 Console 页面的截图(按 F12 可打开控制台)

View File

@ -1,79 +0,0 @@
name: Release
on:
push:
branches:
- master
- "ci/*"
tags:
- v*
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-18.04]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install RPM & Pacman (on Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update &&
sudo apt-get install --no-install-recommends -y rpm &&
sudo apt-get install --no-install-recommends -y bsdtar &&
sudo apt-get install --no-install-recommends -y libopenjp2-tools
- name: Install Snapcraft (on Ubuntu)
uses: samuelmeuli/action-snapcraft@v1
if: startsWith(matrix.os, 'ubuntu')
with:
snapcraft_token: ${{ secrets.snapcraft_token }}
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0
env:
VUE_APP_ELECTRON_API_URL: /api
VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754
VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c
VUE_APP_LASTFM_API_SHARED_SECRET: 307c9fda32b3904e53654baff215cb67
with:
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.github_token }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
use_vue_cli: true
- uses: actions/upload-artifact@v2
with:
name: YesPlayMusic-mac
path: dist_electron/*-universal.dmg
if-no-files-found: ignore
- uses: actions/upload-artifact@v2
with:
name: YesPlayMusic-win
path: dist_electron/*Setup*.exe
if-no-files-found: ignore
- uses: actions/upload-artifact@v2
with:
name: YesPlayMusic-linux
path: dist_electron/*.AppImage
if-no-files-found: ignore

108
.gitignore vendored
View File

@ -1,34 +1,88 @@
.DS_Store
node_modules
/dist
# local env files
.env
.env.local
.env.*.local
# Log files
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.vercel
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
#Electron-builder output
/dist_electron
NeteaseCloudMusicApi-master
NeteaseCloudMusicApi-master.zip
# Coverage directory used by tools like istanbul
coverage
# Local Netlify folder
.netlify
vercel.json
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# ----
dist
**/.tmp
release
.DS_Store
dist-ssr
*.local
.vscode/settings.json

View File

@ -1,3 +0,0 @@
build
coverage
dist

View File

@ -1,12 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "strict"
}

39
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Main(inspector)",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"runtimeArgs": [
"--remote-debugging-port=9222",
"${workspaceFolder}/dist/main/index.cjs",
],
"env": {
"DEBUG": "true",
},
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Main(vite)",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"runtimeArgs": [
"${workspaceFolder}/dist/main/index.cjs",
],
"env": {
"VITE_DEV_SERVER_HOST": "127.0.0.1",
"VITE_DEV_SERVER_PORT": "3344",
},
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"sourceMaps": true
},
]
}

13
.vscode/task.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "debug",
"problemMatcher": [],
"label": "npm: debug",
"detail": "cross-env-shell NODE_ENV=debug \"npm run typecheck && node scripts/build.mjs && vite ./packages/renderer\"",
"group": "build"
}
]
}

View File

@ -1,43 +0,0 @@
FROM node:16.13.1-alpine as build
ENV VUE_APP_NETEASE_API_URL=/api
WORKDIR /app
RUN apk add --no-cache python3 make g++ git
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
FROM nginx:1.20.2-alpine as app
RUN echo $'server { \n\
gzip on;\n\
listen 80; \n\
listen [::]:80; \n\
server_name localhost; \n\
\n\
location / { \n\
root /usr/share/nginx/html; \n\
index index.html; \n\
try_files $uri $uri/ /index.html; \n\
} \n\
\n\
location @rewrites { \n\
rewrite ^(.*)$ /index.html last; \n\
} \n\
\n\
location /api/ { \n\
proxy_set_header Host $host; \n\
proxy_set_header X-Real-IP $remote_addr; \n\
proxy_set_header X-Forwarded-For $remote_addr; \n\
proxy_set_header X-Forwarded-Host $remote_addr; \n\
proxy_set_header X-NginX-Proxy true; \n\
proxy_pass http://localhost:3000/; \n\
} \n\
}' > /etc/nginx/conf.d/default.conf
RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
&& apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \
&& npm i -g NeteaseCloudMusicApi
COPY --from=build /app/dist /usr/share/nginx/html
CMD nginx ; exec npx NeteaseCloudMusicApi

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2022 qier222
Copyright (c) 2021 草鞋没号
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

232
README.md
View File

@ -1,231 +1 @@
<br />
<p align="center">
<a href="https://music.qier222.com" target="blank">
<img src="images/logo.png" alt="Logo" width="156" height="156">
</a>
<h2 align="center" style="font-weight: 600">YesPlayMusic</h2>
<p align="center">
高颜值的第三方网易云播放器
<br />
<a href="https://music.qier222.com" target="blank"><strong>🌎 访问DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a>
<br />
<br />
</p>
</p>
[![Library][library-screenshot]](https://music.qier222.com)
## ✨ 特性
- ✅ 使用 Vue.js 全家桶开发
- 🔴 网易云账号登录(扫码/手机/邮箱登录)
- 📺 支持 MV 播放
- 📃 支持歌词显示
- 📻 支持私人 FM / 每日推荐歌曲
- 🚫🤝 无任何社交功能
- 🌎️ 海外用户可直接播放(需要登录网易云账号)
- 🔐 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server#音源清单),自动使用[各类音源](https://github.com/UnblockNeteaseMusic/server#音源清单)替换变灰歌曲链接 (网页版不支持)
- 「各类音源」指默认启用的音源。
- YouTube 音源需自行安装 `yt-dlp`
- ✔️ 每日自动签到(手机端和电脑端同时签到)
- 🌚 Light/Dark Mode 自动切换
- 👆 支持 Touch Bar
- 🖥️ 支持 PWA可在 Chrome/Edge 里点击地址栏右边的 安装到电脑
- 🟥 支持 Last.fm Scrobble
- ☁️ 支持音乐云盘
- ⌨️ 自定义快捷键和全局快捷键
- 🎧 支持Mpris
- 🛠 更多特性开发中
## 📦️ 安装
Electron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://github.com/qier222) 适配并维护,支持 macOS、Windows、Linux。
访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases)
页面下载安装包。
macOS 用户也可以通过 `brew install --cask yesplaymusic` 来安装。
## ⚙️ 部署至 Vercel
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
1. 部署网易云 API详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85)
。你也可以将 API 部署到 Vercel。
2. 点击本仓库右上角的 Fork复制本仓库到你的 GitHub 账号。
3. 点击仓库的 Add File选择 Create new file输入 `vercel.json`,将下面的内容复制粘贴到文件中,并将 `https://your-netease-api.example.com` 替换为你刚刚部署的网易云 API 地址:
```json
{
"rewrites": [
{
"source": "/api/:match*",
"destination": "https://your-netease-api.example.com/:match*"
}
]
}
```
4. 打开 [Vercel.com](https://vercel.com),使用 GitHub 登录。
5. 点击 Import Git Repository 并选择你刚刚复制的仓库并点击 Import。
6. 点击 PERSONAL ACCOUNT 旁边的 Select。
7. 点击 Environment Variables填写 Name 为 `VUE_APP_NETEASE_API_URL`Value 为 `/api`,点击 Add。最后点击底部的 Deploy 就可以部署到
Vercel 了。
## ⚙️ 部署到自己的服务器
除了部署到 Vercel你还可以部署到自己的服务器上
1. 部署网易云 API详情参见 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
2. 克隆本仓库
```sh
git clone --recursive https://github.com/qier222/YesPlayMusic.git
```
3. 安装依赖
```sh
yarn install
```
4. (可选)使用 Nginx 反向代理 API将 API 路径映射为 `/api`,如果 API 和网页不在同一个域名下的话(跨域),会有一些 bug。
5. 复制 `/.env.example` 文件为 `/.env`,修改里面 `VUE_APP_NETEASE_API_URL` 的值为网易云 API 地址。本地开发的话可以填写 API 地址为 `http://localhost:3000`YesPlayMusic 地址为 `http://localhost:8080`。如果你使用了反向代理 API可以填写 API 地址为 `/api`
```
VUE_APP_NETEASE_API_URL=http://localhost:3000
```
6. 编译打包
```sh
yarn run build
```
7. 将 `/dist` 目录下的文件上传到你的 Web 服务器
## ⚙️ Docker 部署
1. 构建 Docker Image
```sh
docker build -t yesplaymusic .
```
2. 启动 Docker Container
```sh
docker run -d --name YesPlayMusic -p 80:80 yesplaymusic
```
3. Docker Compose 启动
```sh
docker-compose up -d
```
YesPlayMusic 地址为 `http://localhost`
## 👷‍♂️ 打包客户端
如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。
1. 打包 Electron 需要用到 Node.js 和 Yarn。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包。安装 Node.js
后可在终端里执行 `npm install -g yarn` 来安装 Yarn。
2. 使用 `git clone --recursive https://github.com/qier222/YesPlayMusic.git` 克隆本仓库到本地。
3. 使用 `yarn install` 安装项目依赖。
4. 复制 `/.env.example` 文件为 `/.env`
5. 选择下列表格的命令来打包适合的你的安装包,打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli)
| 命令 | 说明 |
| ------------------------------------------ | ------------------------- |
| `yarn electron:build --windows nsis:ia32` | Windows 32 位 |
| `yarn electron:build --windows nsis:arm64` | Windows ARM |
| `yarn electron:build --linux deb:armv7l` | Debian armv7l树莓派等 |
| `yarn electron:build --macos dir:arm64` | macOS ARM |
## :computer: 配置开发环境
本项目由 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 提供 API。
运行本项目
```shell
# 安装依赖
yarn install
# 创建本地环境变量
cp .env.example .env
# 运行(网页端)
yarn serve
# 运行electron
yarn electron:serve
```
本地运行 NeteaseCloudMusicApi或者将 API [部署至 Vercel](#%EF%B8%8F-部署至-vercel)
```shell
# 运行 API (默认 3000 端口)
yarn netease_api:run
```
## ☑️ Todo
查看 Todo 请访问本项目的 [Projects](https://github.com/qier222/YesPlayMusic/projects/1)
欢迎提 Issue 和 Pull request。
## 📜 开源许可
本项目仅供个人学习研究使用,禁止用于商业及非法用途。
基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。
## 灵感来源
API 源代码来自 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [Spotify](https://www.spotify.com)
- [网易云音乐](https://music.163.com)
## 🖼️ 截图
![lyrics][lyrics-screenshot]
![library-dark][library-dark-screenshot]
![album][album-screenshot]
![home-2][home-2-screenshot]
![artist][artist-screenshot]
![search][search-screenshot]
![home][home-screenshot]
![explore][explore-screenshot]
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[album-screenshot]: images/album.png
[artist-screenshot]: images/artist.png
[explore-screenshot]: images/explore.png
[home-screenshot]: images/home.png
[home-2-screenshot]: images/home-2.png
[lyrics-screenshot]: images/lyrics.png
[library-screenshot]: images/library.png
[library-dark-screenshot]: images/library-dark.png
[search-screenshot]: images/search.png
# YesPlayMusic 2.0

View File

@ -1,11 +0,0 @@
module.exports = {
presets: [
[
'@vue/cli-plugin-babel/preset',
{
useBuiltIns: 'usage',
shippedProposals: true,
},
],
],
};

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
build/installerIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
build/uninstallerIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,9 +0,0 @@
services:
YesPlayMusic:
build:
context: .
image: yesplaymusic
container_name: YesPlayMusic
ports:
- 80:80
restart: always

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

View File

@ -1,14 +0,0 @@
{
// @
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -1,130 +1,85 @@
{
"name": "yesplaymusic",
"version": "0.4.4",
"name": "yesplamusic",
"productName": "YesPlayMusic",
"private": true,
"description": "A third party music player for Netease Music",
"author": "qier222<qier222@outlook.com>",
"version": "2.0.0",
"description": "A nifty third-party NetEase Music player",
"homepage": "https://github.com/qier222/YesPlayMusic",
"license": "MIT",
"author": "qier222 <qier222@outlook.com>",
"repository": "github:qier222/YesPlayMusic",
"main": "dist/main/index.cjs",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build -p never",
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
"electron:build-mac": "vue-cli-service electron:build -p never -m",
"electron:build-win": "vue-cli-service electron:build -p never -w",
"electron:build-linux": "vue-cli-service electron:build -p never -l",
"electron:serve": "vue-cli-service electron:serve",
"electron:buildicon": "electron-icon-builder --input=./build/icons/icon.png --output=build --flatten",
"electron:publish": "vue-cli-service electron:build -mwl -p always",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"prettier": "npx prettier --write ./src",
"netease_api:run": "npx NeteaseCloudMusicApi"
"dev": "node scripts/watch.mjs",
"build": "npm run typecheck && node scripts/build.mjs && electron-builder --config .electron-builder.config.js",
"typecheck": "tsc --noEmit --project packages/renderer/tsconfig.json",
"debug": "cross-env-shell NODE_ENV=debug \"npm run typecheck && node scripts/build.mjs && vite ./packages/renderer\"",
"eslint": "eslint --ext .ts,.js ./",
"prettier": "prettier --write './**/*.{ts,js,tsx,jsx}'"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"main": "background.js",
"dependencies": {
"@unblockneteasemusic/server": "v0.27.0-rc.4",
"NeteaseCloudMusicApi": "^4.5.2",
"axios": "^0.21.0",
"NeteaseCloudMusicApi": "^4.5.8",
"change-case": "^4.1.2",
"cli-color": "^2.0.0",
"color": "^3.1.3",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"dayjs": "^1.8.36",
"dexie": "^3.0.3",
"discord-rich-presence": "^0.0.8",
"electron": "^13.6.7",
"electron-builder": "^23.0.0",
"electron-context-menu": "^2.3.0",
"electron-debug": "^3.1.0",
"electron-devtools-installer": "^3.2",
"electron-icon-builder": "^1.0.2",
"electron-is-dev": "^1.2.0",
"electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"express": "^4.17.1",
"express-fileupload": "^1.2.0",
"express-http-proxy": "^1.6.2",
"extract-zip": "^2.0.1",
"howler": "^2.2.3",
"js-cookie": "^2.2.1",
"jsbi": "^4.1.0",
"lodash": "^4.17.20",
"md5": "^2.3.0",
"mpris-service": "^2.1.2",
"music-metadata": "^7.5.3",
"node-vibrant": "^3.2.1-alpha.1",
"nprogress": "^0.2.0",
"pac-proxy-agent": "^4.1.0",
"plyr": "^3.6.2",
"prettier": "2.5.1",
"qrcode": "^1.4.4",
"register-service-worker": "^1.7.1",
"svg-sprite-loader": "^5.0.0",
"tunnel": "^0.0.6",
"vscode-codicons": "^0.0.17",
"vue": "^2.6.11",
"vue-analytics": "^5.22.1",
"vue-clipboard2": "^0.3.1",
"vue-i18n": "^8.22.0",
"vue-router": "^3.4.3",
"vue-slider-component": "^3.2.5",
"vuex": "^3.4.0",
"x11": "^2.3.0"
"cookie-parser": "^1.4.6",
"electron-log": "^4.4.6",
"electron-store": "^8.0.1",
"express": "^4.17.3",
"realm": "^10.13.0"
},
"devDependencies": {
"@types/node": "^17.0.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.9.0",
"husky": "^4.3.0",
"sass": "^1.26.11",
"sass-loader": "^10.0.2",
"vue-cli-plugin-electron-builder": "~2.1.1",
"vue-template-compiler": "^2.6.11"
},
"resolutions": {
"icon-gen": "3.0.0",
"degenerator": "2.2.0",
"electron-builder": "^23.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true,
"browser": true
},
"extends": [
"plugin:vue/essential",
"plugin:vue/recommended",
"plugin:prettier/recommended",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"globals": {
"ipcRenderer": "off"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"husky": {
"hooks": {
"pre-commit": "npm run prettier"
}
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@types/express": "^4.17.13",
"@types/howler": "^2.2.6",
"@types/js-cookie": "^3.0.1",
"@types/lodash-es": "^4.17.6",
"@types/md5": "^2.3.2",
"@types/qrcode": "^1.4.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@vitejs/plugin-react": "^1.2.0",
"ahooks": "^3.1.13",
"ansi-styles": "^6.1.0",
"autoprefixer": "^10.4.2",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"color.js": "^1.2.0",
"colord": "^2.9.2",
"cross-env": "^7.0.3",
"dayjs": "^1.10.8",
"electron": "^17.0.0",
"electron-builder": "^22.14.13",
"electron-devtools-installer": "^3.2.0",
"eslint": "^8.10.0",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react-hooks": "^4.3.0",
"howler": "^2.2.3",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"postcss": "^8.4.7",
"prettier": "2.5.1",
"prettier-plugin-tailwindcss": "^0.1.8",
"qrcode": "^1.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.2.0",
"react-query": "^3.34.16",
"react-router-dom": "^6.2.2",
"react-use": "^17.3.2",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.9",
"tailwindcss": "^3.0.23",
"typescript": "^4.6.2",
"unplugin-auto-import": "^0.6.4",
"valtio": "^1.3.1",
"valtio-persist": "^1.0.2",
"vite": "^2.8.0",
"vite-plugin-resolve": "^1.5.2",
"vite-plugin-svg-icons": "^2.0.1"
}
}

136
packages/main/index.ts Normal file
View File

@ -0,0 +1,136 @@
import {
BrowserWindow,
BrowserWindowConstructorOptions,
app,
shell,
} from 'electron'
import installExtension, {
REACT_DEVELOPER_TOOLS,
REDUX_DEVTOOLS,
} from 'electron-devtools-installer'
import Store from 'electron-store'
import { release } from 'os'
import { join } from 'path'
import Realm from 'realm'
import logger from './logger'
import './server'
const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin'
const isLinux = process.platform === 'linux'
const isDev = !app.isPackaged
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
interface TypedElectronStore {
window: {
width: number
height: number
x?: number
y?: number
}
}
const store = new Store<TypedElectronStore>({
defaults: {
window: {
width: 1440,
height: 960,
},
},
})
let win: BrowserWindow | null = null
async function createWindow() {
// Create window
const options: BrowserWindowConstructorOptions = {
title: 'Main window',
webPreferences: {
preload: join(__dirname, '../preload/index.cjs'),
},
width: store.get('window.width'),
height: store.get('window.height'),
minWidth: 1080,
minHeight: 720,
vibrancy: 'fullscreen-ui',
titleBarStyle: 'hiddenInset',
}
if (store.get('window')) {
options.x = store.get('window.x')
options.y = store.get('window.y')
}
win = new BrowserWindow(options)
// Web server
if (app.isPackaged || process.env['DEBUG']) {
win.loadFile(join(__dirname, '../renderer/index.html'))
} else {
const url = `http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`
logger.info(`[index] Vite dev server running at: ${url}`)
win.loadURL(url)
win.webContents.openDevTools()
}
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' }
})
// Save window position
const saveBounds = () => {
const bounds = win?.getBounds()
if (bounds) {
store.set('window', bounds)
}
}
win.on('resized', saveBounds)
win.on('moved', saveBounds)
}
app.whenReady().then(async () => {
createWindow()
// Install devtool extension
if (isDev) {
installExtension(REACT_DEVELOPER_TOOLS.id).catch(err =>
console.log('An error occurred: ', err)
)
installExtension(REDUX_DEVTOOLS.id).catch(err =>
console.log('An error occurred: ', err)
)
}
})
app.on('window-all-closed', () => {
win = null
if (process.platform !== 'darwin') app.quit()
})
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
})
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
})

29
packages/main/logger.ts Normal file
View File

@ -0,0 +1,29 @@
import styles from 'ansi-styles'
import { app } from 'electron'
import logger from 'electron-log'
const color = (hex: string, text: string) => {
return `${styles.color.ansi(styles.hexToAnsi(hex))}${text}${
styles.color.close
}`
}
logger.transports.console.format = `${color(
'38bdf8',
'[main]'
)} {h}:{i}:{s}.{ms}{scope} {text}`
logger.transports.file.level = app.isPackaged ? 'info' : 'debug'
logger.info(
color(
'335eea',
`\n\n██╗ ██╗███████╗███████╗██████╗ ██╗ █████╗ ██╗ ██╗███╗ ███╗██╗ ██╗███████╗██╗ ██████╗
\n`
)
)
export default logger

45
packages/main/server.ts Normal file
View File

@ -0,0 +1,45 @@
import { pathCase } from 'change-case'
import cookieParser from 'cookie-parser'
import express, { Request, Response } from 'express'
import logger from './logger'
const neteaseApi = require('NeteaseCloudMusicApi')
const app = express()
app.use(cookieParser())
const port = Number(process.env['ELECTRON_DEV_NETEASE_API_PORT'] ?? 3000)
Object.entries(neteaseApi).forEach(([name, handler]) => {
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) {
return
}
const wrappedHandler = async (req: Request, res: Response) => {
logger.info(`[server] Handling request: ${req.path}`)
try {
const result = await handler({
...req.query,
// cookie:
// 'MUSIC_U=1239b6c1217d8cd240df9c8fa15e99a62f9aaac86baa7a8aa3166acbad267cd8a237494327fc3ec043124f3fcebe94e446b14e3f0c3f8af9fe5c85647582a507',
// cookie: req.headers.cookie,
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
})
res.send(result.body)
} catch (error) {
res.status(500).send(error)
}
}
app.get(
`/netease/${pathCase(name)}`,
async (req: Request, res: Response) => await wrappedHandler(req, res)
)
app.post(
`/netease/${pathCase(name)}`,
async (req: Request, res: Response) => await wrappedHandler(req, res)
)
})
app.listen(port, () => {
logger.info(`[server] API server listening on port ${port}`)
})

View File

@ -0,0 +1,32 @@
import dotenv from 'dotenv'
import { builtinModules } from 'module'
import path from 'path'
import { defineConfig } from 'vite'
import pkg from '../../package.json'
import esm2cjs from '../../scripts/vite-plugin-esm2cjs'
dotenv.config({
path: path.resolve(process.cwd(), '.env'),
})
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/main',
emptyOutDir: true,
lib: {
entry: 'index.ts',
formats: ['cjs'],
fileName: () => '[name].cjs',
},
minify: process.env./* from mode option */ NODE_ENV === 'production',
sourcemap: process.env./* from mode option */ NODE_ENV === 'debug',
rollupOptions: {
external: [
'electron',
...builtinModules,
...Object.keys(pkg.dependencies || {}),
],
},
},
})

36
packages/preload/index.ts Normal file
View File

@ -0,0 +1,36 @@
import { contextBridge, ipcRenderer } from 'electron'
import fs from 'fs'
import { useLoading } from './loading'
import { domReady } from './utils'
const { appendLoading, removeLoading } = useLoading()
;(async () => {
await domReady()
appendLoading()
})()
// --------- Expose some API to the Renderer process. ---------
contextBridge.exposeInMainWorld('fs', fs)
contextBridge.exposeInMainWorld('removeLoading', removeLoading)
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
function withPrototype(obj: Record<string, any>) {
const protos = Object.getPrototypeOf(obj)
for (const [key, value] of Object.entries(protos)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) continue
if (typeof value === 'function') {
// Some native APIs, like `NodeJS.EventEmitter['on']`, don't work in the Renderer process. Wrapping them into a function.
obj[key] = function (...args: any) {
return value.call(obj, ...args)
}
} else {
obj[key] = value
}
}
return obj
}

View File

@ -0,0 +1,54 @@
/**
* https://tobiasahlin.com/spinkit
* https://connoratherton.com/loaders
* https://projects.lukehaas.me/css-loaders
* https://matejkustec.github.io/SpinThatShit
*/
export function useLoading() {
const className = `loaders-css__square-spin`
const styleContent = `
@keyframes square-spin {
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
}
.${className} > div {
animation-fill-mode: both;
width: 50px;
height: 50px;
background: #fff;
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
}
.app-loading-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #282c34;
z-index: 9;
}
`
const oStyle = document.createElement('style')
const oDiv = document.createElement('div')
oStyle.id = 'app-loading-style'
oStyle.innerHTML = styleContent
oDiv.className = 'app-loading-wrap'
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
return {
appendLoading() {
document.head.appendChild(oStyle)
document.body.appendChild(oDiv)
},
removeLoading() {
document.head.removeChild(oStyle)
document.body.removeChild(oDiv)
},
}
}

16
packages/preload/utils.ts Normal file
View File

@ -0,0 +1,16 @@
/** Document ready */
export const domReady = (
condition: DocumentReadyState[] = ['complete', 'interactive']
) => {
return new Promise(resolve => {
if (condition.includes(document.readyState)) {
resolve(true)
} else {
document.addEventListener('readystatechange', () => {
if (condition.includes(document.readyState)) {
resolve(true)
}
})
}
})
}

View File

@ -0,0 +1,30 @@
import dotenv from 'dotenv'
import { builtinModules } from 'module'
import path from 'path'
import { defineConfig } from 'vite'
import pkg from '../../package.json'
dotenv.config({
path: path.resolve(process.cwd(), '.env'),
})
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/preload',
emptyOutDir: true,
lib: {
entry: 'index.ts',
formats: ['cjs'],
fileName: () => '[name].cjs',
},
minify: process.env./* from mode option */ NODE_ENV === 'production',
rollupOptions: {
external: [
'electron',
...builtinModules,
...Object.keys(pkg.dependencies || {}),
],
},
},
})

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/public/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>YesPlayMusic</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,36 @@
import { Toaster } from 'react-hot-toast'
import { QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import Player from '@/components/Player'
import Sidebar from '@/components/Sidebar'
import reactQueryClient from '@/utils/reactQueryClient'
import Main from './components/Main'
const App = () => {
return (
<QueryClientProvider client={reactQueryClient}>
<div id="layout" className="grid select-none grid-cols-[16rem_auto]">
<Sidebar />
<Main />
<Player />
</div>
<Toaster position="bottom-center" containerStyle={{ bottom: '5rem' }} />
{/* Devtool */}
<ReactQueryDevtools
initialIsOpen={false}
toggleButtonProps={{
style: {
position: 'fixed',
right: '0',
left: 'auto',
bottom: '4rem',
},
}}
/>
</QueryClientProvider>
)
}
export default App

View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
export enum AlbumApiNames {
FETCH_ALBUM = 'fetchAlbum',
}
// 专辑详情
export interface FetchAlbumParams {
id: number
}
interface FetchAlbumResponse {
code: number
resourceState: boolean
album: Album
songs: Track[]
description: string
}
export function fetchAlbum(
params: FetchAlbumParams,
noCache: boolean
): Promise<FetchAlbumResponse> {
const otherParams: { timestamp?: number } = {}
if (noCache) otherParams.timestamp = new Date().getTime()
return request({
url: '/album',
method: 'get',
params: { ...params, ...otherParams },
})
}

View File

@ -0,0 +1,51 @@
import request from '@/utils/request'
export enum ArtistApiNames {
FETCH_ARTIST = 'fetchArtist',
FETCH_ARTIST_ALBUMS = 'fetchArtistAlbums',
}
// 歌手详情
export interface FetchArtistParams {
id: number
}
interface FetchArtistResponse {
code: number
more: boolean
artist: Artist
hotSongs: Track[]
}
export function fetchArtist(
params: FetchArtistParams,
noCache: boolean
): Promise<FetchArtistResponse> {
const otherParams: { timestamp?: number } = {}
if (noCache) otherParams.timestamp = new Date().getTime()
return request({
url: '/artists',
method: 'get',
params: { ...params, ...otherParams },
})
}
// 获取歌手的专辑列表
export interface FetchArtistAlbumsParams {
id: number
limit?: number // default: 50
offset?: number // default: 0
}
interface FetchArtistAlbumsResponse {
code: number
hotAlbums: Album[]
more: boolean
artist: Artist
}
export function fetchArtistAlbums(
params: FetchArtistAlbumsParams
): Promise<FetchArtistAlbumsResponse> {
return request({
url: 'artist/album',
method: 'get',
params,
})
}

View File

@ -0,0 +1,115 @@
import type { fetchUserAccountResponse } from '@/api/user'
import request from '@/utils/request'
// 手机号登录
interface LoginWithPhoneParams {
countrycode: number | string
phone: string
password?: string
md5_password?: string
captcha?: string | number
}
export interface LoginWithPhoneResponse {
loginType: number
code: number
cookie: string
}
export function loginWithPhone(
params: LoginWithPhoneParams
): Promise<LoginWithPhoneResponse> {
return request({
url: '/login/cellphone',
method: 'post',
params,
})
}
// 邮箱登录
export interface LoginWithEmailParams {
email: string
password?: string
md5_password?: string
}
export interface loginWithEmailResponse extends fetchUserAccountResponse {
code: number
cookie: string
loginType: number
token: string
binding: {
bindingTime: number
expired: boolean
expiresIn: number
id: number
refreshTime: number
tokenJsonStr: string
type: number
url: string
userId: number
}[]
}
export function loginWithEmail(
params: LoginWithEmailParams
): Promise<loginWithEmailResponse> {
return request({
url: '/login',
method: 'post',
params,
})
}
// 生成二维码key
export interface fetchLoginQrCodeKeyResponse {
code: number
data: {
code: number
unikey: string
}
}
export function fetchLoginQrCodeKey(): Promise<fetchLoginQrCodeKeyResponse> {
return request({
url: '/login/qr/key',
method: 'get',
params: {
timestamp: new Date().getTime(),
},
})
}
// 二维码检测扫码状态接口
// 说明: 轮询此接口可获取二维码扫码状态,800为二维码过期,801为等待扫码,802为待确认,803为授权登录成功(803状态码下会返回cookies)
export interface CheckLoginQrCodeStatusParams {
key: string
}
export interface CheckLoginQrCodeStatusResponse {
code: number
message?: string
cookie?: string
}
export function checkLoginQrCodeStatus(
params: CheckLoginQrCodeStatusParams
): Promise<CheckLoginQrCodeStatusResponse> {
return request({
url: '/login/qr/check',
method: 'get',
params: {
key: params.key,
timestamp: new Date().getTime(),
},
})
}
// 刷新登录
export function refreshCookie() {
return request({
url: '/login/refresh',
method: 'post',
})
}
// 退出登录
export function logout() {
return request({
url: '/logout',
method: 'post',
})
}

View File

@ -0,0 +1,57 @@
import request from '@/utils/request'
export enum PlaylistApiNames {
FETCH_PLAYLIST = 'fetchPlaylist',
FETCH_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists',
}
// 歌单详情
export interface FetchPlaylistParams {
id: number
s?: number // 歌单最近的 s 个收藏者
}
interface FetchPlaylistResponse {
code: number
playlist: Playlist
privileges: unknown // TODO: unknown type
relatedVideos: null
resEntrance: null
sharedPrivilege: null
urls: null
}
export function fetchPlaylist(
params: FetchPlaylistParams,
noCache: boolean
): Promise<FetchPlaylistResponse> {
const otherParams: { timestamp?: number } = {}
if (noCache) otherParams.timestamp = new Date().getTime()
if (!params.s) params.s = 0 // 网易云默认返回8个收藏者这里设置为0减少返回的JSON体积
return request({
url: '/playlist/detail',
method: 'get',
params: {
...params,
...otherParams,
},
})
}
// 推荐歌单
interface FetchRecommendedPlaylistsParams {
limit?: number
}
export interface FetchRecommendedPlaylistsResponse {
code: number
category: number
hasTaste: boolean
result: Playlist[]
}
export function fetchRecommendedPlaylists(
params: FetchRecommendedPlaylistsParams
): Promise<FetchRecommendedPlaylistsResponse> {
return request({
url: '/personalized',
method: 'get',
params,
})
}

View File

@ -0,0 +1,100 @@
import request from '@/utils/request'
export enum SearchApiNames {
SEARCH = 'search',
MULTI_MATCH_SEARCH = 'multiMatchSearch',
}
// 搜索
export enum SearchTypes {
SINGLE = 1,
ALBUM = 10,
ARTIST = 100,
PLAYLIST = 1000,
USER = 1002,
MV = 1004,
LYRICS = 1006,
RADIO = 1009,
VIDEO = 1014,
ALL = 1018,
}
export interface SearchParams {
keywords: string
limit?: number // 返回数量 , 默认为 30
offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
type?: SearchTypes // type: 搜索类型
}
interface SearchResponse {
code: number
result: {
album: {
albums: Album[]
more: boolean
moreText: string
resourceIds: number[]
}
artist: {
artists: Artist[]
more: boolean
moreText: string
resourceIds: number[]
}
playList: {
playLists: Playlist[]
more: boolean
moreText: string
resourceIds: number[]
}
song: {
songs: Track[]
more: boolean
moreText: string
resourceIds: number[]
}
user: {
users: User[]
more: boolean
moreText: string
resourceIds: number[]
}
circle: unknown
new_mlog: unknown
order: string[]
rec_type: null
rec_query: null[]
sim_query: unknown
voice: unknown
voiceList: unknown
}
}
export function search(params: SearchParams): Promise<SearchResponse> {
return request({
url: '/search',
method: 'get',
params: params,
})
}
// 搜索多重匹配
export interface MultiMatchSearchParams {
keywords: string
}
interface MultiMatchSearchResponse {
code: number
result: {
album: Album[]
artist: Artist[]
playlist: Playlist[]
orpheus: unknown
orders: Array<'artist' | 'album'>
}
}
export function multiMatchSearch(
params: MultiMatchSearchParams
): Promise<MultiMatchSearchResponse> {
return request({
url: '/search/multimatch',
method: 'get',
params: params,
})
}

View File

@ -0,0 +1,73 @@
import request from '@/utils/request'
export enum TrackApiNames {
FETCH_TRACKS = 'fetchTracks',
FETCH_AUDIO_SOURCE = 'fetchAudioSource',
}
// 获取歌曲详情
export interface FetchTracksParams {
ids: number[]
}
interface FetchTracksResponse {
code: number
songs: Track[]
privileges: {
[key: string]: unknown
}
}
export function fetchTracks(
params: FetchTracksParams
): Promise<FetchTracksResponse> {
return request({
url: '/song/detail',
method: 'get',
params: {
ids: params.ids.join(','),
},
})
}
// 获取音源URL
export interface FetchAudioSourceParams {
id: number
br?: number // bitrate, default 999000320000 = 320kbps
}
interface FetchAudioSourceResponse {
code: number
data: {
br: number
canExtend: boolean
code: number
encodeType: 'mp3' | null
expi: number
fee: number
flag: number
freeTimeTrialPrivilege: {
[key: string]: unknown
}
freeTrialPrivilege: {
[key: string]: unknown
}
freeTrialInfo: null
gain: number
id: number
level: 'standard' | 'null'
md5: string | null
payed: number
size: number
type: 'mp3' | null
uf: null
url: string | null
urlSource: number
}[]
}
export function fetchAudioSource(
params: FetchAudioSourceParams
): Promise<FetchAudioSourceResponse> {
return request({
url: '/song/url',
method: 'get',
params,
})
}

View File

@ -1,4 +1,10 @@
import request from '@/utils/request';
import request from '@/utils/request'
export enum UserApiNames {
FETCH_USER_ACCOUNT = 'fetchUserAccount',
FETCH_USER_LIKED_SONGS_IDS = 'fetchUserLikedSongsIDs',
FETCH_USER_PLAYLISTS = 'fetchUserPlaylists',
}
/**
*
@ -14,74 +20,118 @@ export function userDetail(uid) {
uid,
timestamp: new Date().getTime(),
},
});
})
}
/**
*
* 说明 : 登录后调用此接口 ,
*/
export function userAccount() {
// 获取账号详情
export interface fetchUserAccountResponse {
code: number
account: {
anonimousUser: boolean
ban: number
baoyueVersion: number
createTime: number
donateVersion: number
id: number
paidFee: boolean
status: number
tokenVersion: number
type: number
userName: string
vipType: number
whitelistAuthority: number
} | null
profile: {
userId: number
userType: number
nickname: string
avatarImgId: number
avatarUrl: string
backgroundImgId: number
backgroundUrl: string
signature: string
createTime: number
userName: string
accountType: number
shortUserName: string
birthday: number
authority: number
gender: number
accountStatus: number
province: number
city: number
authStatus: number
description: string | null
detailDescription: string | null
defaultAvatar: boolean
expertTags: [] | null
experts: [] | null
djStatus: number
locationStatus: number
vipType: number
followed: boolean
mutual: boolean
authenticated: boolean
lastLoginTime: number
lastLoginIP: string
remarkName: string | null
viptypeVersion: number
authenticationTypes: number
avatarDetail: string | null
anchor: boolean
} | null
}
export function fetchUserAccount(): Promise<fetchUserAccountResponse> {
return request({
url: '/user/account',
method: 'get',
params: {
timestamp: new Date().getTime(),
},
});
})
}
/**
*
* 说明 : 登录后调用此接口 , id,
* - uid : 用户 id
* - limit : 返回数量 , 30
* - offset : 偏移数量 , :( -1)*30, 30 limit , 0
* @param {Object} params
* @param {number} params.uid
* @param {number} params.limit
* @param {number=} params.offset
*/
export function userPlaylist(params) {
// 获取用户歌单
export interface FetchUserPlaylistsParams {
uid: number
offset: number
limit?: number // default 30
}
interface FetchUserPlaylistsResponse {
code: number
more: false
version: string
playlist: Playlist[]
}
export function fetchUserPlaylists(
params: FetchUserPlaylistsParams
): Promise<FetchUserPlaylistsResponse> {
return request({
url: '/user/playlist',
method: 'get',
params,
});
})
}
/**
*
* 说明 : 登录后调用此接口 , id,
* - uid : 用户 id
* - type : type=1 weekData, type=0 allData
* @param {Object} params
* @param {number} params.uid
* @param {number} params.type
*/
export function userPlayHistory(params) {
return request({
url: '/user/record',
method: 'get',
params,
});
export interface FetchUserLikedSongsIDsParams {
uid: number
}
/**
*
* 说明 : 调用此接口 , id, id列表(id数组)
* - uid: 用户 id
* @param {number} uid
*/
export function userLikedSongsIDs(uid) {
interface FetchUserLikedSongsIDsResponse {
code: number
checkPoint: number
ids: number[]
}
export function fetchUserLikedSongsIDs(
params: FetchUserLikedSongsIDsParams
): Promise<FetchUserLikedSongsIDsResponse> {
return request({
url: '/likelist',
method: 'get',
params: {
uid,
uid: params.uid,
timestamp: new Date().getTime(),
},
});
})
}
/**
@ -98,7 +148,7 @@ export function dailySignin(type = 0) {
type,
timestamp: new Date().getTime(),
},
});
})
}
/**
@ -118,7 +168,7 @@ export function likedAlbums(params) {
limit: params.limit,
timestamp: new Date().getTime(),
},
});
})
}
/**
@ -133,7 +183,7 @@ export function likedArtists(params) {
limit: params.limit,
timestamp: new Date().getTime(),
},
});
})
}
/**
@ -148,15 +198,15 @@ export function likedMVs(params) {
limit: params.limit,
timestamp: new Date().getTime(),
},
});
})
}
/**
*
*/
export function uploadSong(file) {
let formData = new FormData();
formData.append('songFile', file);
let formData = new FormData()
formData.append('songFile', file)
return request({
url: '/cloud',
method: 'post',
@ -169,8 +219,8 @@ export function uploadSong(file) {
},
timeout: 200000,
}).catch(error => {
alert(`上传失败Error: ${error}`);
});
alert(`上传失败Error: ${error}`)
})
}
/**
@ -183,12 +233,12 @@ export function uploadSong(file) {
* @param {number=} params.offset
*/
export function cloudDisk(params = {}) {
params.timestamp = new Date().getTime();
params.timestamp = new Date().getTime()
return request({
url: '/user/cloud',
method: 'get',
params,
});
})
}
/**
@ -202,7 +252,7 @@ export function cloudDiskTrackDetail(id) {
timestamp: new Date().getTime(),
id,
},
});
})
}
/**
@ -217,5 +267,5 @@ export function cloudDiskTrackDelete(id) {
timestamp: new Date().getTime(),
id,
},
});
})
}

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="chevron-left" class="svg-inline--fa fa-chevron-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M224 480c-8.188 0-16.38-3.125-22.62-9.375l-192-192c-12.5-12.5-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L77.25 256l169.4 169.4c12.5 12.5 12.5 32.75 0 45.25C240.4 476.9 232.2 480 224 480z"></path></svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="chevron-up" class="svg-inline--fa fa-chevron-up fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z"></path></svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="48px" height="48px"><circle cx="12" cy="12" r="10" opacity=".35"/><path d="M15.839,6.559l-5.7,1.9c-0.793,0.264-1.416,0.887-1.68,1.68l-1.9,5.7c-0.33,0.99,0.612,1.933,1.603,1.603l5.7-1.9 c0.793-0.264,1.416-0.887,1.68-1.68l1.9-5.7C17.771,7.171,16.829,6.228,15.839,6.559z M12,14c-1.105,0-2-0.895-2-2 c0-1.105,0.895-2,2-2s2,0.895,2,2C14,13.105,13.105,14,12,14z"/></svg>

After

Width:  |  Height:  |  Size: 433 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="thumbs-down" class="svg-inline--fa fa-thumbs-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M96 32.04H32c-17.67 0-32 14.32-32 31.1v223.1c0 17.67 14.33 31.1 32 31.1h64c17.67 0 32-14.33 32-31.1V64.03C128 46.36 113.7 32.04 96 32.04zM467.3 240.2C475.1 231.7 480 220.4 480 207.9c0-23.47-16.87-42.92-39.14-47.09C445.3 153.6 448 145.1 448 135.1c0-21.32-14-39.18-33.25-45.43C415.5 87.12 416 83.61 416 79.98C416 53.47 394.5 32 368 32h-58.69c-34.61 0-68.28 11.22-95.97 31.98L179.2 89.57C167.1 98.63 160 112.9 160 127.1l.1074 160c0 0-.0234-.0234 0 0c.0703 13.99 6.123 27.94 17.91 37.36l16.3 13.03C276.2 403.9 239.4 480 302.5 480c30.96 0 49.47-24.52 49.47-48.11c0-15.15-11.76-58.12-34.52-96.02H464c26.52 0 48-21.47 48-47.98C512 262.5 492.2 241.9 467.3 240.2z"></path></svg>

After

Width:  |  Height:  |  Size: 889 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="24" height="24"
viewBox="0 0 24 24"
style=" fill:#000000;"><path d="M19.971,14.583C19.985,14.39,20,14.197,20,14c0-0.513-0.053-1.014-0.145-1.5C19.947,12.014,20,11.513,20,11 c0-4.418-3.582-8-8-8s-8,3.582-8,8c0,0.513,0.053,1.014,0.145,1.5C4.053,12.986,4,13.487,4,14c0,0.197,0.015,0.39,0.029,0.583 C3.433,14.781,3,15.337,3,16c0,0.828,0.672,1.5,1.5,1.5c0.103,0,0.203-0.01,0.3-0.03C6.093,20.148,8.827,22,12,22 s5.907-1.852,7.2-4.53c0.097,0.02,0.197,0.03,0.3,0.03c0.828,0,1.5-0.672,1.5-1.5C21,15.337,20.567,14.781,19.971,14.583z" opacity=".35"></path><path d="M21,18h-2v-6h2c1.105,0,2,0.895,2,2v2C23,17.105,22.105,18,21,18z"></path><path d="M3,12h2v6H3c-1.105,0-2-0.895-2-2v-2C1,12.895,1.895,12,3,12z"></path><path d="M5,13c0-0.843,0-1.638,0-2c0-3.866,3.134-7,7-7s7,3.134,7,7c0,0.362,0,1.157,0,2h2c0-0.859,0-1.617,0-2c0-4.971-4.029-9-9-9 s-9,4.029-9,9c0,0.383,0,1.141,0,2H5z"></path></svg>

After

Width:  |  Height:  |  Size: 946 B

Some files were not shown because too many files have changed in this diff Show More