refactor: version 2.0 (React)
|
@ -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
|
|
@ -1,8 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
46
.electron-builder.config.js
Normal 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
|
@ -0,0 +1,3 @@
|
|||
ELECTRON_WEB_SERVER_PORT = 42710
|
||||
ELECTRON_DEV_NETEASE_API_PORT = 3000
|
||||
VITE_APP_NETEASE_API_URL = /netease
|
|
@ -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
|
@ -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',
|
||||
},
|
||||
}
|
21
.github/ISSUE_TEMPLATE/----------.md
vendored
|
@ -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 可打开控制台)
|
79
.github/workflows/build.yaml
vendored
|
@ -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
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
build
|
||||
coverage
|
||||
dist
|
12
.prettierrc
|
@ -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
|
@ -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
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
43
Dockerfile
|
@ -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
|
2
LICENSE
|
@ -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
|
@ -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> |
|
||||
<a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a> |
|
||||
<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
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
{
|
||||
useBuiltIns: 'usage',
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 523 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 750 B |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 965 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 353 KiB |
Before Width: | Height: | Size: 276 KiB |
Before Width: | Height: | Size: 1.1 KiB |
BIN
build/installerIcon.ico
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
build/uninstallerIcon.ico
Normal file
After Width: | Height: | Size: 36 KiB |
|
@ -1,9 +0,0 @@
|
|||
services:
|
||||
YesPlayMusic:
|
||||
build:
|
||||
context: .
|
||||
image: yesplaymusic
|
||||
container_name: YesPlayMusic
|
||||
ports:
|
||||
- 80:80
|
||||
restart: always
|
BIN
images/album.png
Before Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 228 KiB |
Before Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 312 KiB |
BIN
images/home.png
Before Width: | Height: | Size: 389 KiB |
Before Width: | Height: | Size: 335 KiB |
Before Width: | Height: | Size: 324 KiB |
BIN
images/logo.png
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 339 KiB |
Before Width: | Height: | Size: 276 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
// 支持 @ 的别名解析
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
195
package.json
|
@ -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
|
@ -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
|
@ -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
|
@ -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}`)
|
||||
})
|
32
packages/main/vite.config.ts
Normal 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
|
@ -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
|
||||
}
|
54
packages/preload/loading.ts
Normal 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
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
30
packages/preload/vite.config.ts
Normal 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 || {}),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
17
packages/renderer/index.html
Normal 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>
|
BIN
packages/renderer/public/fonts/Manrope-Bold.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-ExtraBold.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-ExtraLight.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-Light.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-Medium.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-Regular.ttf
Executable file
BIN
packages/renderer/public/fonts/Manrope-SemiBold.ttf
Executable file
BIN
packages/renderer/public/fonts/PlusJakartaSans-Bold.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-BoldItalic.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-ExtraBold.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-ExtraLight.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-Italic.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-Light.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-LightItalic.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-Medium.woff2
Normal file
BIN
packages/renderer/public/fonts/PlusJakartaSans-Regular.woff2
Normal file
36
packages/renderer/src/App.tsx
Normal 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
|
29
packages/renderer/src/api/album.ts
Normal 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 },
|
||||
})
|
||||
}
|
51
packages/renderer/src/api/artist.ts
Normal 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,
|
||||
})
|
||||
}
|
115
packages/renderer/src/api/auth.ts
Normal 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',
|
||||
})
|
||||
}
|
57
packages/renderer/src/api/playlist.ts
Normal 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,
|
||||
})
|
||||
}
|
100
packages/renderer/src/api/search.ts
Normal 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,
|
||||
})
|
||||
}
|
73
packages/renderer/src/api/track.ts
Normal 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 999000,320000 = 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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
1
packages/renderer/src/assets/icons/back.svg
Normal 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 |
1
packages/renderer/src/assets/icons/chevron-up.svg
Normal 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 |
1
packages/renderer/src/assets/icons/compass.svg
Normal 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 |
1
packages/renderer/src/assets/icons/dislike.svg
Normal 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 |
4
packages/renderer/src/assets/icons/dj.svg
Normal 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 |