Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2422d826 | ||
|
|
6b9ba74c35 | ||
|
|
22e2653e20 | ||
|
|
4cac54c84a | ||
|
|
e5f9ecd7b5 | ||
|
|
d64cfb40ec | ||
|
|
53c30caf00 | ||
|
|
6fcce91b2b | ||
|
|
5a53dfcdf7 | ||
|
|
bbe71bd62d | ||
|
|
ecedf0b7b9 | ||
|
|
f2935f3c1c | ||
|
|
d3e677d494 | ||
|
|
e49f90b0da | ||
|
|
0359b0e470 | ||
|
|
aa2373695b | ||
|
|
cf940ff405 | ||
|
|
e7c17cf531 | ||
|
|
c6a1ca4f42 | ||
|
|
8f5008df69 | ||
|
|
428ce4be86 | ||
|
|
d46c4c4285 | ||
|
|
0b871175b2 | ||
|
|
c34c4fd880 | ||
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 | ||
|
|
c012f84064 | ||
|
|
a57a18b9f5 | ||
|
|
309c323a14 | ||
|
|
6a1e606d6d | ||
|
|
af3931847e | ||
|
|
41eadb5843 | ||
|
|
8963d719d9 | ||
|
|
0a7761ffff | ||
|
|
1a63771f2d | ||
|
|
1f9141ba33 | ||
|
|
a341a69d48 | ||
|
|
0cedfe0af3 | ||
|
|
59f492ed8f | ||
|
|
8f416ff841 | ||
|
|
99ab194e4b | ||
|
|
43fb9b48dc | ||
|
|
c61e54d6a3 | ||
|
|
c8d195053f | ||
|
|
8cfe5d0481 | ||
|
|
fcc2f5015f | ||
|
|
9b98a45264 | ||
|
|
3c4e836fb8 | ||
|
|
a8111b9d3f | ||
|
|
8eaeffeda3 | ||
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 | ||
|
|
ee9bbf0687 | ||
|
|
693dc65b07 | ||
|
|
354d271582 | ||
|
|
eaaeb0f5d3 | ||
|
|
e4e8deec59 | ||
|
|
3f21704b82 | ||
|
|
7ad2bb8bde | ||
|
|
3123e4f5f8 | ||
|
|
60e43c9f40 | ||
|
|
298813a057 | ||
|
|
2db74f3a39 | ||
|
|
024ff1773e | ||
|
|
6046e5a153 | ||
|
|
3c39dbd87f | ||
|
|
c5747b6a3e | ||
|
|
750d570c3d | ||
|
|
b811b00b9f | ||
|
|
a372570038 | ||
|
|
6d5fa15098 | ||
|
|
b65369a8a6 | ||
|
|
0af0ac3cce | ||
|
|
f0ed78eed5 | ||
|
|
b1cda68c75 | ||
|
|
dd1081cfa2 | ||
|
|
046b8f3a92 | ||
|
|
72650a5419 | ||
|
|
d471e686b5 | ||
|
|
41c4342f76 | ||
|
|
16802aaac7 | ||
|
|
740ffa32d6 | ||
|
|
dc119654a2 | ||
|
|
e82ffe8683 | ||
|
|
2e31578fb6 | ||
|
|
1fc8cd73c8 | ||
|
|
4c34d02666 | ||
|
|
7eb63e888f | ||
|
|
41837fdba7 | ||
|
|
8d2aea1efc | ||
|
|
571640bf7d | ||
|
|
4b3aeea5f4 | ||
|
|
02641ec4f1 | ||
|
|
7da55779b4 | ||
|
|
5a98cc64a9 | ||
|
|
e4e0af1d9d | ||
|
|
b435b2152e | ||
|
|
57763066a0 | ||
|
|
dc8cd56f89 | ||
|
|
9c024c18ea | ||
|
|
74f503b6ec | ||
|
|
3841a48119 | ||
|
|
477de06585 | ||
|
|
479802df07 | ||
|
|
f4ea51c761 | ||
|
|
9fa5935929 | ||
|
|
ab0fb19585 | ||
|
|
540b0c5855 | ||
|
|
b5ec3aaee3 | ||
|
|
91d66801a9 | ||
|
|
73470ef643 | ||
|
|
b5f193c731 | ||
|
|
f13eaf838f | ||
|
|
7f8dbbaaf8 | ||
|
|
846691f789 | ||
|
|
17bb9f13b8 | ||
|
|
261997a627 | ||
|
|
bf3675ce45 | ||
|
|
da2a924c7e | ||
|
|
ea245945fa | ||
|
|
6ac8b948b2 | ||
|
|
995f087e1d | ||
|
|
021ace4a86 | ||
|
|
2c2301d8af | ||
|
|
ee7fe27801 | ||
|
|
73798ba196 | ||
|
|
c30263b019 | ||
|
|
f96b07a43c | ||
|
|
3c64da89f6 | ||
|
|
0207b92f2a | ||
|
|
0ee896d352 | ||
|
|
0e0a3911c1 | ||
|
|
b5cde21981 | ||
|
|
ffbe6229f9 | ||
|
|
bf1312889d | ||
|
|
2bf3d7db5a | ||
|
|
06ccb969e4 | ||
|
|
6867897e02 | ||
|
|
c78f94ae86 | ||
|
|
40ed0dada1 | ||
|
|
f4d2c5f337 | ||
|
|
aace9e97b0 | ||
|
|
b3641801df | ||
|
|
1dd877832c | ||
|
|
660cd33387 | ||
|
|
8caebf65f9 | ||
|
|
3b432dbd8b | ||
|
|
ee934c89f4 | ||
|
|
921b0eed0a | ||
|
|
a60e557ba2 | ||
|
|
82aabc555a | ||
|
|
8149c2dd71 | ||
|
|
02084c6be0 | ||
|
|
ade3ddbe82 | ||
|
|
c8cb4c2c9e | ||
|
|
396a54f646 | ||
|
|
cf8fd6b7fc | ||
|
|
4709ab3910 | ||
|
|
a9a03e1cc4 | ||
|
|
ecdb3b75ba | ||
|
|
415cf3b3c9 | ||
|
|
5425288e16 | ||
|
|
ae8f3696f3 | ||
|
|
80aea0826b | ||
|
|
a65a7224ae |
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
# 根配置文件
|
||||
## 编辑器在查找配置时会停止查找更高层次的配置文件
|
||||
root = true
|
||||
|
||||
# 通配符,匹配所有文件
|
||||
[*]
|
||||
# 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码
|
||||
charset = utf-8
|
||||
# 使用空格作为缩进风格
|
||||
indent_style = space
|
||||
# 设置每个缩进级别的空格数量为 2
|
||||
indent_size = 2
|
||||
# 设置行尾换行符为LF(Line Feed)
|
||||
end_of_line = lf
|
||||
# 在文件的末尾插入一个新行
|
||||
insert_final_newline = true
|
||||
# 删除每一行末尾的尾随空格
|
||||
trim_trailing_whitespace = true
|
||||
15
.env
@@ -1,15 +0,0 @@
|
||||
# 全局 API 地址
|
||||
VITE_MUSIC_API = "https://api-music.imsyy.top/"
|
||||
|
||||
# ICP 备案号
|
||||
## 若不需要,请设为空即可
|
||||
VITE_ICP = "豫ICP备2022018134号-1"
|
||||
|
||||
# 公告配置
|
||||
## 若无需公告,请将任意一项设为空即可
|
||||
## 公告标题
|
||||
VITE_ANN_TITLE = "即将完成"
|
||||
## 公告内容
|
||||
VITE_ANN_CONTENT = "进行最后完善"
|
||||
## 公告时长(毫秒)
|
||||
VITE_ANN_DURATION = 3000
|
||||
38
.env.example
Normal file
@@ -0,0 +1,38 @@
|
||||
# 程序配置
|
||||
## 程序名称
|
||||
MAIN_VITE_TITLE = "SPlayer"
|
||||
## 程序主端口
|
||||
MAIN_VITE_MAIN_PORT = 7899
|
||||
## 程序开发环境运行端口
|
||||
MAIN_VITE_DEV_PORT = 6944
|
||||
|
||||
# 全局 API 配置
|
||||
## API 运行地址
|
||||
MAIN_VITE_SERVER_HOST = 127.0.0.1
|
||||
## API 运行端口
|
||||
MAIN_VITE_SERVER_PORT = 11451
|
||||
## API 在线地址( 网址结尾不要加 / )
|
||||
### 用于非客户端( 浏览器环境 )
|
||||
RENDERER_VITE_SERVER_URL = /api
|
||||
|
||||
# 程序信息
|
||||
RENDERER_VITE_SITE_TITLE = "SPlayer"
|
||||
RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
|
||||
# 公告配置
|
||||
## 若无需公告,请将标题或内容任意一项设为空即可
|
||||
## 公告类型
|
||||
RENDERER_VITE_ANN_TYPE = "info"
|
||||
## 公告标题
|
||||
RENDERER_VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
RENDERER_VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
RENDERER_VITE_ANN_DURATION = 8000
|
||||
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
56
.eslintrc.cjs
Normal file
@@ -0,0 +1,56 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"@electron-toolkit",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
rules: {
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
},
|
||||
ignorePatterns: [
|
||||
"node_modules/",
|
||||
"build/",
|
||||
"dist/",
|
||||
"out/",
|
||||
"components.d.ts",
|
||||
"auto-imports.d.ts",
|
||||
],
|
||||
globals: {
|
||||
defineProps: true,
|
||||
defineEmits: true,
|
||||
withDefaults: true,
|
||||
h: true,
|
||||
vue: true,
|
||||
ref: true,
|
||||
reactive: true,
|
||||
computed: true,
|
||||
watch: true,
|
||||
provide: true,
|
||||
inject: true,
|
||||
defineComponent: true,
|
||||
onBeforeMount: true,
|
||||
onBeforeUnmount: true,
|
||||
onUnmounted: true,
|
||||
onMounted: true,
|
||||
nextTick: true,
|
||||
watchEffect: true,
|
||||
electron: true,
|
||||
$message: true,
|
||||
$dialog: true,
|
||||
$loadingBar: true,
|
||||
$changeLogin: true,
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
$refreshCloudCatch: true,
|
||||
$cleanAll: true,
|
||||
$player: true,
|
||||
},
|
||||
};
|
||||
35
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "是网页端还是客户端"
|
||||
placeholder: "客户端"
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前系统环境"
|
||||
placeholder: "win11"
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前 Node.js 及 npm 版本"
|
||||
placeholder: "v18.16.0 / v9.6.7"
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前版本"
|
||||
placeholder: "v1.0.0"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 添加功能
|
||||
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
|
||||
about: 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
62
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 仅部署 Win 端
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and electron app
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App
|
||||
run: npm run build:win
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 清理不必要的构建产物
|
||||
- name: Cleanup Artifacts
|
||||
run: |
|
||||
npx rimraf "dist/!(*.exe)"
|
||||
# 上传构建产物
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
name: ${{ github.ref }}-rc
|
||||
body: This version is still under development, currently only provides windows version, non-developers please do not use!
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: dist/*.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
46
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
imsyy/splayer
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
154
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
# Release 发行版本部署
|
||||
## 多端部署
|
||||
name: Build Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
# Windows
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Windows
|
||||
run: npm run build:win || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for macOS
|
||||
run: npm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
run: sudo apt-get update
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Linux
|
||||
run: npm run build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
6
.gitignore
vendored
@@ -13,12 +13,12 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
out
|
||||
.env
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
.vscode
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
|
||||
5
.npmrc
Normal file
@@ -0,0 +1,5 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
shamefully-hoist=true
|
||||
8
.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
||||
out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
8
.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
# 是否使用单引号而不是双引号
|
||||
singleQuote: false
|
||||
# 是否在语句末尾使用分号
|
||||
semi: true
|
||||
# 每行的最大打印宽度
|
||||
printWidth: 100
|
||||
# 是否在对象和数组的末尾加上逗号
|
||||
trailingComma: all
|
||||
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# build
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.25.3-alpine-slim as app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
674
LICENSE
@@ -1,21 +1,661 @@
|
||||
MIT License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2023 imsyy
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
441
README.md
@@ -1,13 +1,28 @@
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的在线音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
# SPlayer
|
||||
|
||||
> 本项目采用 Vue 3 全家桶及 SCSS 开发
|
||||
> 目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
|
||||
> 一个简约的音乐播放器
|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ### 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
|
||||
|
||||
- 欢迎各位大佬 `Star` 😍
|
||||
|
||||
## 👀 Demo
|
||||
|
||||
@@ -15,115 +30,194 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 账号
|
||||
- 扫码登录
|
||||
- 手机号登录(目前暂时无法使用)
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 管理
|
||||
- 下载歌曲(最高 Hi-Res)
|
||||
- 新建歌单
|
||||
- 歌单编辑
|
||||
- 收藏 / 取消收藏歌单
|
||||
- 收藏 / 取消收藏歌手
|
||||
- 推荐
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 音乐云盘
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 播放
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 实验性功能,需在设置中开启 )
|
||||
- 音乐渐入渐出
|
||||
- 其他
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- 移动端基础适配
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 🎨 封面主题色自适应
|
||||
- 🌚 Light / Dark 模式自动切换
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲(最高支持 Hi-Res)
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
- 📻 私人 FM
|
||||
- ☁️ 云盘音乐上传
|
||||
- 📂 云盘内歌曲播放
|
||||
- 🔄 云盘内歌曲纠正
|
||||
- 🗑️ 云盘歌曲删除
|
||||
- 📝 支持逐字歌词
|
||||
- 🔄 歌词滚动以及歌词翻译
|
||||
- 📹 MV 与视频播放
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区及评论点赞
|
||||
- 🌓 明暗模式自动 / 手动切换
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
- [ ] 逐字歌词支持
|
||||
- [ ] 发表评论
|
||||
## 🖼️ screenshots
|
||||
|
||||
## 😍 Screenshots
|
||||
> 开发中,仅供参考
|
||||
|
||||
<details>
|
||||
<summary>主页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>播放页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>发现页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>歌单页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>评论页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## ⚙️ 部署
|
||||
<details>
|
||||
<summary>本地音乐</summary>
|
||||
|
||||
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
|
||||

|
||||
|
||||
### API 服务
|
||||
</details>
|
||||
|
||||
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
|
||||
## 📦️ 获取
|
||||
|
||||
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址
|
||||
### 稳定版
|
||||
|
||||
```js
|
||||
VITE_MUSIC_API = "your api url"
|
||||
```
|
||||
通常情况下,可以在 [Releases](https://github.com/imsyy/SPlayer/releases) 中获取稳定版
|
||||
|
||||
### 安装依赖
|
||||
### 开发版
|
||||
|
||||
可以通过 `GitHub Actions` 工作流获取最新的开发版,目前开发版仅提供 `Win` 版本
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
### 本地构建
|
||||
|
||||
> 请尽量拉取最新分支后使用本地构建方式,在线部署的仓库可能更新不及时
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
# 构建
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 开发
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或者
|
||||
yarn dev
|
||||
# 或者
|
||||
npm dev
|
||||
# 从 Docker Hub 拉取
|
||||
docker pull imsyy/splayer:latest
|
||||
# 从 GitHub ghcr 拉取
|
||||
docker pull ghcr.io/imsyy/splayer:latest
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
```
|
||||
|
||||
### 构建
|
||||
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
构建完成后可将生成的 `dist` 文件夹内的文件上传至服务器
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
```
|
||||
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||

|
||||
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
|
||||
|
||||
```bash
|
||||
git clone https://github.com/example/repository.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 编译打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
## ⚙️ 本地部署
|
||||
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
```bash
|
||||
npm install pnpm -g
|
||||
```
|
||||
|
||||
3. 克隆仓库并拉取至本地,此处不再赘述
|
||||
4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述)
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -131,16 +225,203 @@ npm build
|
||||
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [MIT license](https://opensource.org/license/mit/) 许可进行开源
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
本项目使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途。** 本项目旨在提供一个前端练手的实战项目,用于帮助开发者提升技能水平和对前端技术的理解
|
||||
本项目部分功能使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
|
||||
同时,本项目开发者承诺 **严格遵守相关法律法规和网易云音乐 API 使用协议,不会利用本项目进行任何违法活动。** 如因使用本项目而引起的任何纠纷或责任,均由使用者自行承担。**本项目开发者不承担任何因使用本项目而导致的任何直接或间接责任,并保留追究使用者违法行为的权利**
|
||||
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
|
||||
感谢您的理解
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证
|
||||
3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
<details>
|
||||
<summary>查看目录结构详情</summary>
|
||||
|
||||
> ChatGPT 写的,如有错误,请见谅
|
||||
|
||||
```dir
|
||||
├── auto-imports.d.ts # 自动导入
|
||||
├── components.d.ts # 自动导入
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker
|
||||
├── electron # Electron
|
||||
│ ├── main # Electron 主进程
|
||||
│ │ ├── index.js # 主进程入口
|
||||
│ │ ├── mainIpcMain.js # 主进程与渲染进程通信
|
||||
│ │ ├── startMainServer.js # 启动主进程服务器
|
||||
│ │ ├── startNcmServer.js # 启动网易云音乐服务
|
||||
│ │ └── utils # 主进程工具函数
|
||||
│ │ ├── checkUpdates.js # 检查更新
|
||||
│ │ ├── createGlobalShortcut.js # 创建全局快捷键
|
||||
│ │ ├── createSystemTray.js # 创建系统托盘
|
||||
│ │ ├── getNeteaseMusicUrl.js # 解灰
|
||||
│ │ ├── kwDES.js # DES加密算法
|
||||
│ │ └── readDirAsync.js # 异步读取目录
|
||||
│ └── preload # Electron 预加载脚本
|
||||
│ └── index.js # 预加载脚本入口文件
|
||||
├── electron-builder.yml # Electron Builder
|
||||
├── electron.vite.config.js # Electron Vite
|
||||
├── index.html # 主页面 HTML
|
||||
├── LICENSE # 项目许可证
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── src # 项目源代码
|
||||
│ ├── api # API 相关
|
||||
│ │ ├── ./..
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── assets # 静态资源
|
||||
│ │ ├── emoji.json # 表情数据
|
||||
│ │ ├── icon.json # 图标数据
|
||||
│ │ └── themeColor.json # 主题颜色数据
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── Cover # 封面相关组件目录
|
||||
│ │ │ ├── CoverDropdown.vue # 封面下拉组件
|
||||
│ │ │ ├── MainCover.vue # 主封面组件
|
||||
│ │ │ ├── SpecialCoverCard.vue # 特殊封面卡片组件
|
||||
│ │ │ └── SpecialCover.vue # 特殊封面组件
|
||||
│ │ ├── Global # 全局组件目录
|
||||
│ │ │ ├── MainLayout.vue # 主布局组件
|
||||
│ │ │ ├── Menu.vue # 菜单组件
|
||||
│ │ │ ├── Pagination.vue # 分页组件
|
||||
│ │ │ ├── Playlist.vue # 歌单组件
|
||||
│ │ │ ├── Provider.vue # 全局化配置组件
|
||||
│ │ │ └── SvgIcon.vue # SVG 图标组件
|
||||
│ │ ├── List # 列表组件目录
|
||||
│ │ │ ├── CommentList.vue # 评论列表组件
|
||||
│ │ │ ├── SongListDropdown.vue # 歌曲下拉组件
|
||||
│ │ │ └── SongList.vue # 歌曲列表组件
|
||||
│ │ ├── Modal # 弹窗相关组件目录
|
||||
│ │ │ ├── AddPlaylist.vue # 添加歌单组件
|
||||
│ │ │ ├── CloudSongMatch.vue # 云盘歌曲匹配组件
|
||||
│ │ │ ├── CreatePlaylist.vue # 创建歌单组件
|
||||
│ │ │ ├── DownloadSong.vue # 下载歌曲组件
|
||||
│ │ │ ├── LoginPhone.vue # 手机登录组件
|
||||
│ │ │ ├── LoginQRCode.vue # 二维码登录组件
|
||||
│ │ │ ├── Login.vue # 登录组件
|
||||
│ │ │ ├── PlaylistUpdate.vue # 歌单编辑组件
|
||||
│ │ │ └── UpCloudSong.vue # 上传云盘歌曲组件
|
||||
│ │ ├── Nav # 导航相关组件目录
|
||||
│ │ │ ├── MainNav.vue # 主导航组件
|
||||
│ │ │ └── UserData.vue # 用户数据组件
|
||||
│ │ ├── Player # 播放器相关组件目录
|
||||
│ │ │ ├── CountDown.vue # 倒计时组件
|
||||
│ │ │ ├── FullPlayer.vue # 全屏播放器组件
|
||||
│ │ │ ├── Lyric.vue # 歌词组件
|
||||
│ │ │ ├── MainControl.vue # 主控制组件
|
||||
│ │ │ ├── PlayerControl.vue # 播放器控制组件
|
||||
│ │ │ ├── PlayerCover.vue # 播放器封面组件
|
||||
│ │ │ └── PrivateFm.vue # 私人 FM 组件
|
||||
│ │ ├── Search # 搜索相关组件
|
||||
│ │ │ ├── SearchHot.vue # 热门搜索组件
|
||||
│ │ │ ├── SearchInp.vue # 搜索输入组件
|
||||
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
|
||||
│ │ └── WinDom # 窗口 DOM 相关组件
|
||||
│ │ └── TitleBar.vue # 标题栏组件
|
||||
│ ├── main.js # Vue 应用的入口文件
|
||||
│ ├── router # Vue Router 相关文件夹
|
||||
│ │ ├── index.js # Vue Router 入口文件
|
||||
│ │ └── routes.js # 路由配置文件
|
||||
│ ├── stores # Vuex Store 相关文件夹
|
||||
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
|
||||
│ │ ├── index.js # Vuex Store 入口文件
|
||||
│ │ ├── musicData.js # 音乐数据相关文件
|
||||
│ │ ├── siteData.js # 网站数据相关文件
|
||||
│ │ ├── siteSettings.js # 网站设置相关文件
|
||||
│ │ └── siteStatus.js # 网站状态相关文件
|
||||
│ ├── style # 样式相关文件夹
|
||||
│ │ ├── animate.scss # 动画样式文件
|
||||
│ │ └── main.scss # 主样式文件
|
||||
│ ├── utils # 工具函数文件夹
|
||||
│ │ ├── auth.js # 认证相关函数
|
||||
│ │ ├── base64.js # Base64编码解码相关函数
|
||||
│ │ ├── color-utils.js # 颜色工具函数
|
||||
│ │ ├── cover-color.js # 封面颜色相关函数
|
||||
│ │ ├── debounce.js # 防抖函数
|
||||
│ │ ├── formatData.js # 数据格式化函数
|
||||
│ │ ├── formRules.js # 表单验证规则
|
||||
│ │ ├── globalEvents.js # 全局事件处理函数
|
||||
│ │ ├── globalShortcut.js # 全局快捷键相关函数
|
||||
│ │ ├── helper.js # 辅助函数
|
||||
│ │ ├── parseLyric.js # 解析歌词函数
|
||||
│ │ ├── Player.js # 播放器控制相关函数
|
||||
│ │ ├── request.js # 网络请求相关函数
|
||||
│ │ ├── throttle.js # 节流函数
|
||||
│ │ ├── timeTools.js # 时间工具函数
|
||||
│ │ └── userSignIn.js # 用户登录相关函数
|
||||
│ └── views # Vue组件文件夹
|
||||
│ ├── Artist # 艺术家相关组件
|
||||
│ │ ├── albums.vue # 艺术家专辑组件
|
||||
│ │ ├── hot.vue # 艺术家热门组件
|
||||
│ │ ├── index.vue # 艺术家主组件
|
||||
│ │ ├── songs.vue # 艺术家歌曲组件
|
||||
│ │ └── videos.vue # 艺术家视频组件
|
||||
│ ├── Cloud.vue # 云盘组件
|
||||
│ ├── Comment.vue # 评论组件
|
||||
│ ├── DailySongs.vue # 每日推荐组件
|
||||
│ ├── Discover # 发现音乐相关组件
|
||||
│ │ ├── artists.vue # 发现音乐艺术家组件
|
||||
│ │ ├── index.vue # 发现音乐主组件
|
||||
│ │ ├── new.vue # 发现音乐新歌组件
|
||||
│ │ ├── playlists.vue # 发现音乐歌单组件
|
||||
│ │ └── toplists.vue # 发现音乐排行榜组件
|
||||
│ ├── History.vue # 历史记录组件
|
||||
│ ├── Home.vue # 主页组件
|
||||
│ ├── Like # 我喜欢的相关组件
|
||||
│ │ ├── albums.vue # 我喜欢的专辑组件
|
||||
│ │ ├── artists.vue # 我喜欢的艺术家组件
|
||||
│ │ ├── index.vue # 我喜欢的主组件
|
||||
│ │ ├── playlists.vue # 我喜欢的歌单组件
|
||||
│ │ └── videos.vue # 我喜欢的视频组件
|
||||
│ ├── List # 列表相关组件
|
||||
│ │ ├── album.vue # 专辑组件
|
||||
│ │ └── playlist.vue # 歌单组件
|
||||
│ │ └── dj.vue # 电台组件
|
||||
│ ├── Local # 本地音乐相关组件
|
||||
│ │ ├── albums.vue # 本地音乐专辑组件
|
||||
│ │ ├── artists.vue # 本地音乐艺术家组件
|
||||
│ │ ├── index.vue # 本地音乐主组件
|
||||
│ │ └── songs.vue # 本地音乐歌曲组件
|
||||
│ ├── Player.vue # 视频播放器组件
|
||||
│ ├── Dj # 电台相关组件
|
||||
│ │ └── index.vue # 电台主组件
|
||||
│ │ └── type.vue # 电台分类组件
|
||||
│ ├── Search # 搜索相关组件
|
||||
│ │ ├── albums.vue # 搜索专辑组件
|
||||
│ │ ├── artists.vue # 搜索艺术家组件
|
||||
│ │ ├── index.vue # 搜索主组件
|
||||
│ │ ├── playlists.vue # 搜索歌单组件
|
||||
│ │ ├── songs.vue # 搜索歌曲组件
|
||||
│ │ └── videos.vue # 搜索视频组件
|
||||
│ │ └── djs.vue # 搜索电台组件
|
||||
│ ├── Setting # 设置相关组件
|
||||
│ │ └── index.vue # 设置主组件
|
||||
│ ├── Song.vue
|
||||
│ ├── State
|
||||
│ │ ├── 403.vue
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ └── Test.vue
|
||||
└── vercel.json # Vercel 部署配置
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
70
auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
112
components.d.ts
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
|
||||
CloudSongMatch: typeof import('./src/components/Modal/CloudSongMatch.vue')['default']
|
||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
|
||||
CoverPlayBtn: typeof import('./src/components/Cover/CoverPlayBtn.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
||||
LoginPhone: typeof import('./src/components/Modal/LoginPhone.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
|
||||
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
|
||||
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
|
||||
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
|
||||
MainLayout: typeof import('./src/components/Global/MainLayout.vue')['default']
|
||||
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
|
||||
Menu: typeof import('./src/components/Global/Menu.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
NH6: typeof import('naive-ui')['NH6']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
|
||||
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
|
||||
PrivateFm: typeof import('./src/components/Player/PrivateFm.vue')['default']
|
||||
Provider: typeof import('./src/components/Global/Provider.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchHot: typeof import('./src/components/Search/SearchHot.vue')['default']
|
||||
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
|
||||
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
|
||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||
SongListDrawer: typeof import('./src/components/List/SongListDrawer.vue')['default']
|
||||
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
|
||||
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
|
||||
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
|
||||
Spectrum: typeof import('./src/components/Player/Spectrum.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
|
||||
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
|
||||
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
|
||||
UserData: typeof import('./src/components/Nav/UserData.vue')['default']
|
||||
}
|
||||
}
|
||||
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 7899:7899
|
||||
restart: always
|
||||
98
electron-builder.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
# 应用程序的唯一标识符
|
||||
appId: com.imsyy.splayer
|
||||
# 应用程序的产品名称
|
||||
productName: SPlayer
|
||||
copyright: Copyright © imsyy 2023
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
buildResources: build
|
||||
# 包含在最终应用程序构建中的文件列表,这里使用通配符 ! 表示排除不需要的文件
|
||||
files:
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
||||
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
|
||||
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
# 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack:
|
||||
- public/**
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/imgs/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target: nsis
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
oneClick: false
|
||||
# 安装程序的生成名称
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
# 创建的桌面快捷方式名称
|
||||
shortcutName: ${productName}
|
||||
# 卸载时显示的名称
|
||||
uninstallDisplayName: ${productName}
|
||||
# 创建桌面图标
|
||||
createDesktopShortcut: always
|
||||
# 是否允许 UAC 提升权限
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/imgs/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/imgs/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/imgs/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
# 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false
|
||||
darkModeSupport: true
|
||||
category: public.app-category.music
|
||||
# macOS 平台的 DMG 配置
|
||||
dmg:
|
||||
# DMG 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/imgs/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
# 维护者信息
|
||||
maintainer: imsyy.top
|
||||
# 应用程序类别
|
||||
category: Audio;Music
|
||||
# AppImage 配置
|
||||
appImage:
|
||||
# AppImage 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
# 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false
|
||||
# 自动更新的配置
|
||||
publish:
|
||||
# 更新提供商
|
||||
provider: github
|
||||
# 自动更新检查的 URL
|
||||
# url: https://example.com/auto-updates
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
163
electron.vite.config.mjs
Normal file
@@ -0,0 +1,163 @@
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
defineConfig,
|
||||
externalizeDepsPlugin,
|
||||
loadEnv,
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 读取环境变量
|
||||
const getEnv = (name) => {
|
||||
return loadEnv(mode, process.cwd())[name];
|
||||
};
|
||||
// 返回配置
|
||||
return {
|
||||
// 主进程
|
||||
main: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@main": resolve(__dirname, "electron/main"),
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/main/index.js"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 预渲染
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.mjs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 渲染进程
|
||||
renderer: {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
{
|
||||
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
// viteCompression
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
// PWA
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
workbox: {
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "file-cache",
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "image-cache",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
short_name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
description: getEnv("RENDERER_VITE_SITE_DES"),
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
background_color: "#efefef",
|
||||
icons: [
|
||||
{
|
||||
src: "/imgs/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
port: getEnv("MAIN_VITE_DEV_PORT"),
|
||||
// 代理
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://${getEnv("MAIN_VITE_SERVER_HOST")}:${getEnv("MAIN_VITE_SERVER_PORT")}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 构建
|
||||
root: ".",
|
||||
build: {
|
||||
minify: "terser",
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
pure_funcs: ["console.log"],
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
258
electron/main/index.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import { join } from "path";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
|
||||
import { platform, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import createSystemTray from "@main/utils/createSystemTray";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import Store from "electron-store";
|
||||
import log from "electron-log";
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
// 设置日志文件的最大大小为 2 MB
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
||||
// 绑定 console 事件
|
||||
console.error = log.error.bind(log);
|
||||
console.warn = log.warn.bind(log);
|
||||
console.info = log.info.bind(log);
|
||||
console.debug = log.debug.bind(log);
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
constructor() {
|
||||
// 主窗口
|
||||
this.mainWindow = null;
|
||||
// 主代理
|
||||
this.mainServer = null;
|
||||
// 网易云 API
|
||||
this.ncmServer = null;
|
||||
// Store
|
||||
this.store = new Store({
|
||||
// 窗口大小
|
||||
windowSize: {
|
||||
width: { type: "number", default: 1280 },
|
||||
height: { type: "number", default: 740 },
|
||||
},
|
||||
});
|
||||
// 设置应用程序名称
|
||||
if (process.platform === "win32") app.setAppUserModelId(app.getName());
|
||||
// 初始化
|
||||
this.checkApp().then(async (lockObtained) => {
|
||||
if (lockObtained) {
|
||||
await this.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 单例锁
|
||||
async checkApp() {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("已有一个程序正在运行,本次启动阻止");
|
||||
app.quit();
|
||||
// 未获得锁
|
||||
return false;
|
||||
}
|
||||
// 聚焦到当前程序
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
});
|
||||
// 获得锁
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化程序
|
||||
async init() {
|
||||
log.info("主进程初始化");
|
||||
|
||||
// 启动网易云 API
|
||||
try {
|
||||
this.ncmServer = await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("启动网易云 API 失败:", error);
|
||||
}
|
||||
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) {
|
||||
this.mainServer = await startMainServer();
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("SPlayer");
|
||||
// 应用程序准备好之前注册
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
]);
|
||||
|
||||
// 主应用程序事件
|
||||
this.mainAppEvents();
|
||||
}
|
||||
|
||||
// 创建主窗口
|
||||
createWindow() {
|
||||
// 创建浏览器窗口
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: app.getName() || "SPlayer",
|
||||
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
|
||||
height: this.store.get("windowSize.height") || 740, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
// transparent: true, // 透明窗口
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/imgs/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.once("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 主窗口事件
|
||||
this.mainWindowEvents();
|
||||
|
||||
// 设置窗口打开处理程序
|
||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
this.mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
this.mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
}
|
||||
|
||||
// 配置网络代理
|
||||
const proxyRules = this.store.get("proxy");
|
||||
if (proxyRules) {
|
||||
this.mainWindow.webContents.session.setProxy({ proxyRules }, (result) => {
|
||||
console.info("网络代理配置:", result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.whenReady().then(async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow, this.store);
|
||||
// 系统托盘
|
||||
createSystemTray(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
|
||||
// 开发环境下 F12 打开控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) this.createWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (!platform.isMacOS) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主窗口事件
|
||||
mainWindowEvents() {
|
||||
this.mainWindow.on("show", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("hide", () => {
|
||||
// console.info("窗口隐藏");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("blur", () => {
|
||||
// console.info("窗口失去焦点");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", true);
|
||||
});
|
||||
|
||||
this.mainWindow.on("unmaximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", false);
|
||||
});
|
||||
|
||||
this.mainWindow.on("resized", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
this.mainWindow.on("moved", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 窗口关闭
|
||||
this.mainWindow.on("close", (event) => {
|
||||
if (platform.isLinux) {
|
||||
app.quit();
|
||||
} else {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new MainProcess();
|
||||
321
electron/main/mainIpcMain.js
Normal file
@@ -0,0 +1,321 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { download } from "electron-dl";
|
||||
import { getFonts } from "font-list";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
/**
|
||||
* 监听主进程的 IPC 事件
|
||||
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
||||
* @param {Store} store - 存储对象
|
||||
*/
|
||||
|
||||
const mainIpcMain = (win, store) => {
|
||||
// 窗口操作部分
|
||||
ipcMain.on("window-min", (ev) => {
|
||||
// 阻止最小化
|
||||
ev.preventDefault();
|
||||
// 最小化
|
||||
win.minimize();
|
||||
});
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("windowState", win.isMaximized());
|
||||
});
|
||||
ipcMain.on("window-restore", () => {
|
||||
win.restore();
|
||||
});
|
||||
ipcMain.on("window-hide", () => {
|
||||
win.hide();
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
});
|
||||
ipcMain.on("window-relaunch", () => {
|
||||
app.isQuiting = true;
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
});
|
||||
ipcMain.on("check-updates", () => {
|
||||
console.info("开始检查更新");
|
||||
configureAutoUpdater();
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("setProgressBar", (_, val) => {
|
||||
if (val === "close") {
|
||||
win.setProgressBar(-1);
|
||||
return false;
|
||||
}
|
||||
win.setProgressBar(val / 100);
|
||||
});
|
||||
|
||||
// 解灰
|
||||
ipcMain.handle("getMusicNumUrl", async (_, data) => {
|
||||
// 解析传入数据
|
||||
const songData = JSON.parse(data);
|
||||
const songName = `${songData?.name}-${songData?.artists?.[0].name}`;
|
||||
console.log("开始解灰:", songName);
|
||||
const url = await getNeteaseMusicUrl(songName);
|
||||
console.log("解灰地址:", url);
|
||||
return url;
|
||||
});
|
||||
|
||||
// bili 链接解析
|
||||
ipcMain.handle("getBiliUrlData", async (_, url) => {
|
||||
const data = await getBiliUrlBase64(url);
|
||||
return data;
|
||||
});
|
||||
|
||||
// 默认音乐文件夹
|
||||
ipcMain.handle("getdefaultMusicPath", async () => {
|
||||
const path = app.getPath("music");
|
||||
return path;
|
||||
});
|
||||
|
||||
// 选择文件夹
|
||||
ipcMain.handle("selectDir", async (_, isChooseDl = false) => {
|
||||
try {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
title: isChooseDl ? "选择下载目录" : "选择添加目录",
|
||||
defaultPath: isChooseDl ? app.getPath("downloads") : app.getPath("music"),
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
buttonLabel: "选择文件夹",
|
||||
});
|
||||
if (!canceled) {
|
||||
const selectedDirectory = filePaths[0];
|
||||
return selectedDirectory;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("选择文件夹时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取文件夹内容
|
||||
ipcMain.handle("getDirContents", async (_, selectedDir) => {
|
||||
try {
|
||||
// 使用 readDirAsync 函数递归地读取文件夹内容
|
||||
const directoryContents = await readDirAsync(selectedDir);
|
||||
return directoryContents;
|
||||
} catch (err) {
|
||||
console.error("读取文件夹内容时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐歌词
|
||||
ipcMain.handle("getMusicLyric", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const lyric = data.common.lyrics;
|
||||
if (lyric && lyric.length > 0) {
|
||||
return lyric[0];
|
||||
}
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
// 返回读取的 lrc 数据,如果没有则返回 null
|
||||
return lrcData || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐歌词出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐封面
|
||||
ipcMain.handle("getMusicCover", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const picture = data.common.picture;
|
||||
if (picture && picture.length > 0) {
|
||||
const coverData = picture[0].data;
|
||||
const coverFormat = picture[0].format;
|
||||
return { coverData, coverFormat };
|
||||
}
|
||||
// 如果封面数据不存在,尝试读取同名的封面图片文件
|
||||
else {
|
||||
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
||||
const coverData = await fs.readFile(coverFilePath);
|
||||
// 返回读取的封面图片数据,如果没有则返回 null
|
||||
return coverData ? { coverData, coverFormat: "jpg" } : null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐封面出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 执行复制操作
|
||||
ipcMain.handle("copyData", async (_, data) => {
|
||||
try {
|
||||
clipboard.writeText(data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("复制操作出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 本地磁盘文件删除
|
||||
ipcMain.handle("deleteFile", async (_, path) => {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (fs.access(path)) {
|
||||
// 尝试删除文件
|
||||
fs.unlink(path);
|
||||
console.log(`文件已删除:${path}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`文件删除操作出错:${path}`, err);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开歌曲目录
|
||||
ipcMain.on("openSongLocal", (_, path) => {
|
||||
try {
|
||||
if (fs.access(path)) {
|
||||
shell.showItemInFolder(path);
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("打开歌曲目录时出错:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, songData, options) => {
|
||||
try {
|
||||
const { url, data, lyric, name, type } = JSON.parse(songData);
|
||||
const { path, downloadMeta, downloadCover, downloadLyrics } = JSON.parse(options);
|
||||
if (fs.access(path)) {
|
||||
console.info("开始下载:", name, url);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, url, {
|
||||
directory: path,
|
||||
filename: `${name}.${type}`,
|
||||
});
|
||||
// 若关闭,则不进行元信息写入
|
||||
if (!downloadMeta) return true;
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, data.cover, {
|
||||
directory: path,
|
||||
filename: `${name}.jpg`,
|
||||
});
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = data.name || "未知曲目";
|
||||
songFile.tag.album = data.album?.name || "未知专辑";
|
||||
songFile.tag.performers = data?.artists?.map((ar) => ar.name) || ["未知艺术家"];
|
||||
if (downloadLyrics) songFile.tag.lyrics = lyric;
|
||||
if (downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} else {
|
||||
console.log(`目录不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载文件时出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取系统全部字体
|
||||
ipcMain.handle("getAllFonts", async () => {
|
||||
try {
|
||||
const fonts = await getFonts();
|
||||
return fonts;
|
||||
} catch (error) {
|
||||
console.error("获取系统字体时出错:", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 配置网络代理
|
||||
ipcMain.on("set-proxy", (_, config) => {
|
||||
console.log(config);
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
store.set("proxy", proxyRules);
|
||||
win.webContents.session.setProxy({ proxyRules }, () => {
|
||||
console.info("网络代理配置完成");
|
||||
});
|
||||
});
|
||||
|
||||
// 取消代理
|
||||
ipcMain.on("remove-proxy", () => {
|
||||
store.set("proxy", "");
|
||||
win.webContents.session.setProxy({ proxyRules: "" }, () => {
|
||||
console.info("取消网络代理配置");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 Bilibili 视频中获取文件的 Base64 数据
|
||||
*
|
||||
* @param {string} url - 要获取的文件的 URL
|
||||
* @returns {Promise<string>} - 文件的 Base64 数据
|
||||
*/
|
||||
const getBiliUrlBase64 = async (url) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
Referer: "https://www.bilibili.com/",
|
||||
"User-Agent": "okhttp/3.4.1",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
withCredentials: false,
|
||||
});
|
||||
// 将二进制数据转换为缓冲区
|
||||
const buffer = toBuffer(response.data);
|
||||
// 将缓冲区中的数据转换为 Base64 编码的字符串
|
||||
const encodedData = buffer.toString("base64");
|
||||
// 返回 Base64 编码的文件数据
|
||||
return encodedData;
|
||||
} catch (error) {
|
||||
console.error("获取文件数据时发生错误:" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据转换为缓冲区( Buffer )
|
||||
*
|
||||
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
|
||||
* @returns {Buffer} - 转换后的缓冲区
|
||||
*/
|
||||
const toBuffer = (data) => {
|
||||
if (data instanceof Buffer) {
|
||||
return data;
|
||||
} else {
|
||||
return Buffer.from(data);
|
||||
}
|
||||
};
|
||||
|
||||
export default mainIpcMain;
|
||||
19
electron/main/startMainServer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { join } from "path";
|
||||
import express from "express";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
/**
|
||||
* 启动主服务器
|
||||
* @returns {import('http').Server} HTTP 服务器实例
|
||||
*/
|
||||
export const startMainServer = async () => {
|
||||
const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env;
|
||||
const port = MAIN_VITE_MAIN_PORT ?? 7899;
|
||||
const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`;
|
||||
const expressApp = express();
|
||||
// 代理
|
||||
expressApp.use("/", express.static(join(__dirname, "../renderer/")));
|
||||
expressApp.use("/api", expressProxy(apiHost));
|
||||
// 启动 Express 应用服务器,并监听指定端口
|
||||
return expressApp.listen(port, "127.0.0.1");
|
||||
};
|
||||
20
electron/main/startNcmServer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import netEaseApi from "NeteaseCloudMusicApi";
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=11451] - 服务器端口
|
||||
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
|
||||
* @returns {Promise<void>} 返回一个 Promise,在 API 服务器成功启动后 resolve
|
||||
*/
|
||||
export const startNcmServer = async (
|
||||
options = {
|
||||
port: 11451,
|
||||
host: "127.0.0.1",
|
||||
},
|
||||
) => {
|
||||
console.log(options);
|
||||
return await netEaseApi.serveNcmApi(options);
|
||||
};
|
||||
65
electron/main/utils/checkUpdates.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { dialog } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import pkg from "electron-updater";
|
||||
|
||||
const { autoUpdater } = pkg;
|
||||
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否立即下载并安装新版本?",
|
||||
buttons: ["立即下载", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 触发手动下载
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
if (is.dev) return false;
|
||||
|
||||
// 监听下载进度事件
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
console.log(`更新下载进度: ${progressObj.percent}%`);
|
||||
});
|
||||
|
||||
// 下载完成
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
// 显示安装弹窗
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "下载完成",
|
||||
message: "新版本已下载完成,是否现在安装?",
|
||||
buttons: ["是", "稍后"],
|
||||
type: "question",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 下载失败
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.error("下载更新失败:", err);
|
||||
dialog.showErrorBox("下载更新失败", "请检查网络连接并稍后重试!");
|
||||
});
|
||||
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
hasNewVersion(info);
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
};
|
||||
24
electron/main/utils/createGlobalShortcut.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { globalShortcut } from "electron";
|
||||
|
||||
/**
|
||||
* 注册全局快捷键
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CmdOrCtrl+Shift+R", () => {
|
||||
if (win && win.isFocused()) win?.reload();
|
||||
});
|
||||
|
||||
// 打开开发者工具
|
||||
globalShortcut.register("CmdOrCtrl+Shift+I", () => {
|
||||
if (win && win.isFocused()) {
|
||||
win?.webContents.openDevTools({
|
||||
mode: "right",
|
||||
activate: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default createGlobalShortcut;
|
||||
139
electron/main/utils/createSystemTray.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
// 当前歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统托盘
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemTray = (win) => {
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(
|
||||
nativeImage
|
||||
.createFromPath(
|
||||
join(
|
||||
__dirname,
|
||||
process.platform === "win32"
|
||||
? "../../public/imgs/icons/favicon.ico"
|
||||
: "../../public/imgs/icons/favicon-32x32.png",
|
||||
),
|
||||
)
|
||||
.resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
}),
|
||||
);
|
||||
// 应用内菜单
|
||||
Menu.setApplicationMenu(createTrayMenu(win));
|
||||
// 默认名称
|
||||
win.setTitle(app.getName());
|
||||
mainTray.setTitle(app.getName());
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 左键事件
|
||||
mainTray.on("click", () => win.show());
|
||||
// 托盘菜单
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
// 系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放歌曲改变
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
win.setTitle(val);
|
||||
mainTray.setTitle(val);
|
||||
mainTray.setToolTip(val);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放状态改变
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/imgs/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/imgs/icons/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 返回菜单
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
click() {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("showPlayer");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
accelerator: "CmdOrCtrl+Space",
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("open-setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "退出",
|
||||
icon: createIcon("power"),
|
||||
click: () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export default createSystemTray;
|
||||
167
electron/main/utils/getNeteaseMusicUrl.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { encryptQuery } from "@main/utils/kwDES";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 网易云音乐解灰
|
||||
*/
|
||||
|
||||
// 咪咕音乐请求头
|
||||
const requestHeader = {
|
||||
Origin: "http://music.migu.cn/",
|
||||
Referer: "http://m.music.migu.cn/v3/",
|
||||
aversionid: import.meta.env.MAIN_VITE_MIGU_COOKIE || null,
|
||||
channel: "0146921",
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongId = async (keyword) => {
|
||||
try {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("咪咕解灰歌曲 ID:", songId);
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
const songUrl = result.data.data.url;
|
||||
console.info("咪咕解灰歌曲 URL:", songUrl);
|
||||
return songUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongId = async (keyword) => {
|
||||
try {
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8" +
|
||||
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword.toString();
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("酷我解灰歌曲 ID:", songId);
|
||||
const url = encryptQuery
|
||||
? "http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
"corp=kuwo&source=kwplayer_ar_1.1.9_oppo_118980_320.apk&p2p=1&type=convert_url2&sig=0&format=mp3" +
|
||||
"&rid=" +
|
||||
songId,
|
||||
)
|
||||
: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_" +
|
||||
songId;
|
||||
const result = await axios.get(url, { "user-agent": "okhttp/3.10.0" });
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
console.info("酷我解灰歌曲 URL:", urlMatch);
|
||||
return urlMatch;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取给定关键字的音乐 URL
|
||||
* @param {string} keyword - 关键字
|
||||
* @returns {Promise<?string>} 音乐 URL
|
||||
*/
|
||||
const getNeteaseMusicUrl = async (keyword) => {
|
||||
try {
|
||||
const [kuwoSongUrl, miguSongUrl] = await Promise.all([
|
||||
getKuwoSongUrl(keyword),
|
||||
getMiguSongUrl(keyword),
|
||||
]);
|
||||
if (kuwoSongUrl) {
|
||||
return kuwoSongUrl;
|
||||
}
|
||||
if (miguSongUrl) {
|
||||
return miguSongUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取解灰 URL 全部失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default getNeteaseMusicUrl;
|
||||
574
electron/main/utils/kwDES.js
Normal file
@@ -0,0 +1,574 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*
|
||||
Thanks to
|
||||
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
|
||||
https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
|
||||
*/
|
||||
|
||||
const Long = (n) => {
|
||||
const bN = BigInt(n);
|
||||
|
||||
return {
|
||||
low: Number(bN),
|
||||
valueOf: () => bN.valueOf(),
|
||||
toString: () => bN.toString(),
|
||||
not: () => Long(~bN),
|
||||
isNegative: () => bN < 0,
|
||||
or: (x) => Long(bN | BigInt(x)),
|
||||
and: (x) => Long(bN & BigInt(x)),
|
||||
xor: (x) => Long(bN ^ BigInt(x)),
|
||||
equals: (x) => bN === BigInt(x),
|
||||
multiply: (x) => Long(bN * BigInt(x)),
|
||||
shiftLeft: (x) => Long(bN << BigInt(x)),
|
||||
shiftRight: (x) => Long(bN >> BigInt(x)),
|
||||
};
|
||||
};
|
||||
|
||||
const range = (n) => Array.from(new Array(n).keys());
|
||||
const power = (base, index) =>
|
||||
Array(index)
|
||||
.fill(null)
|
||||
.reduce((result) => result.multiply(base), Long(1));
|
||||
const LongArray = (...array) => array.map((n) => (n === -1 ? Long(-1, -1) : Long(n)));
|
||||
|
||||
// EXPANSION
|
||||
const arrayE = LongArray(
|
||||
31,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
-1,
|
||||
-1,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
-1,
|
||||
-1,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
-1,
|
||||
-1,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
-1,
|
||||
-1,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
-1,
|
||||
-1,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
// INITIAL_PERMUTATION
|
||||
const arrayIP = LongArray(
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
63,
|
||||
55,
|
||||
47,
|
||||
39,
|
||||
31,
|
||||
23,
|
||||
15,
|
||||
7,
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
);
|
||||
|
||||
// INVERSE_PERMUTATION
|
||||
const arrayIP_1 = LongArray(
|
||||
39,
|
||||
7,
|
||||
47,
|
||||
15,
|
||||
55,
|
||||
23,
|
||||
63,
|
||||
31,
|
||||
38,
|
||||
6,
|
||||
46,
|
||||
14,
|
||||
54,
|
||||
22,
|
||||
62,
|
||||
30,
|
||||
37,
|
||||
5,
|
||||
45,
|
||||
13,
|
||||
53,
|
||||
21,
|
||||
61,
|
||||
29,
|
||||
36,
|
||||
4,
|
||||
44,
|
||||
12,
|
||||
52,
|
||||
20,
|
||||
60,
|
||||
28,
|
||||
35,
|
||||
3,
|
||||
43,
|
||||
11,
|
||||
51,
|
||||
19,
|
||||
59,
|
||||
27,
|
||||
34,
|
||||
2,
|
||||
42,
|
||||
10,
|
||||
50,
|
||||
18,
|
||||
58,
|
||||
26,
|
||||
33,
|
||||
1,
|
||||
41,
|
||||
9,
|
||||
49,
|
||||
17,
|
||||
57,
|
||||
25,
|
||||
32,
|
||||
0,
|
||||
40,
|
||||
8,
|
||||
48,
|
||||
16,
|
||||
56,
|
||||
24,
|
||||
);
|
||||
|
||||
// ROTATES
|
||||
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
|
||||
const arrayLsMask = LongArray(0, 0x100001, 0x300003);
|
||||
const arrayMask = range(64).map((n) => power(2, n));
|
||||
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1);
|
||||
|
||||
// PERMUTATION
|
||||
const arrayP = LongArray(
|
||||
15,
|
||||
6,
|
||||
19,
|
||||
20,
|
||||
28,
|
||||
11,
|
||||
27,
|
||||
16,
|
||||
0,
|
||||
14,
|
||||
22,
|
||||
25,
|
||||
4,
|
||||
17,
|
||||
30,
|
||||
9,
|
||||
1,
|
||||
7,
|
||||
23,
|
||||
13,
|
||||
31,
|
||||
26,
|
||||
2,
|
||||
8,
|
||||
18,
|
||||
12,
|
||||
29,
|
||||
5,
|
||||
21,
|
||||
10,
|
||||
3,
|
||||
24,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE1
|
||||
const arrayPC_1 = LongArray(
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE2
|
||||
const arrayPC_2 = LongArray(
|
||||
13,
|
||||
16,
|
||||
10,
|
||||
23,
|
||||
0,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
2,
|
||||
27,
|
||||
14,
|
||||
5,
|
||||
20,
|
||||
9,
|
||||
-1,
|
||||
-1,
|
||||
22,
|
||||
18,
|
||||
11,
|
||||
3,
|
||||
25,
|
||||
7,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
6,
|
||||
26,
|
||||
19,
|
||||
12,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
40,
|
||||
51,
|
||||
30,
|
||||
36,
|
||||
46,
|
||||
54,
|
||||
-1,
|
||||
-1,
|
||||
29,
|
||||
39,
|
||||
50,
|
||||
44,
|
||||
32,
|
||||
47,
|
||||
-1,
|
||||
-1,
|
||||
43,
|
||||
48,
|
||||
38,
|
||||
55,
|
||||
33,
|
||||
52,
|
||||
-1,
|
||||
-1,
|
||||
45,
|
||||
41,
|
||||
49,
|
||||
35,
|
||||
28,
|
||||
31,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
const matrixNSBox = [
|
||||
[
|
||||
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12, 7,
|
||||
8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4,
|
||||
2, 11, 14, 1, 7, 8, 13,
|
||||
],
|
||||
[
|
||||
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13, 6, 4,
|
||||
1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7,
|
||||
1, 10, 12, 14, 2, 5, 9,
|
||||
],
|
||||
[
|
||||
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7, 12,
|
||||
5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9,
|
||||
0, 14, 3, 10, 7, 1, 12,
|
||||
],
|
||||
[
|
||||
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5, 14, 10,
|
||||
13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5,
|
||||
6, 12, 11, 3, 8, 9, 14,
|
||||
],
|
||||
[
|
||||
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15, 5,
|
||||
6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4,
|
||||
12, 7, 10, 9, 1, 13, 6, 3,
|
||||
],
|
||||
[
|
||||
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4, 10,
|
||||
8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2,
|
||||
12, 14, 7, 5, 10, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7, 8,
|
||||
13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7,
|
||||
8, 12, 15, 10, 7, 6, 12,
|
||||
],
|
||||
[
|
||||
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14, 13,
|
||||
1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8,
|
||||
7, 11, 0, 4, 13, 2, 11,
|
||||
],
|
||||
];
|
||||
|
||||
const bitTransform = (arrInt, n, l) => {
|
||||
// int[], int, long : long
|
||||
let l2 = Long(0);
|
||||
range(n).forEach((i) => {
|
||||
if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0)) return;
|
||||
l2 = l2.or(arrayMask[i]);
|
||||
});
|
||||
return l2;
|
||||
};
|
||||
|
||||
const DES64 = (longs, l) => {
|
||||
const pR = range(8).map(() => Long(0));
|
||||
const pSource = [Long(0), Long(0)];
|
||||
let L = Long(0);
|
||||
let R = Long(0);
|
||||
let out = bitTransform(arrayIP, 64, l);
|
||||
pSource[0] = out.and(0xffffffff);
|
||||
pSource[1] = out.and(-4294967296).shiftRight(32);
|
||||
|
||||
range(16).forEach((i) => {
|
||||
let SOut = Long(0);
|
||||
|
||||
R = Long(pSource[1]);
|
||||
R = bitTransform(arrayE, 64, R);
|
||||
R = R.xor(longs[i]);
|
||||
range(8).forEach((j) => {
|
||||
pR[j] = R.shiftRight(j * 8).and(255);
|
||||
});
|
||||
range(8)
|
||||
.reverse()
|
||||
.forEach((sbi) => {
|
||||
SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]);
|
||||
});
|
||||
R = bitTransform(arrayP, 32, SOut);
|
||||
L = Long(pSource[0]);
|
||||
pSource[0] = Long(pSource[1]);
|
||||
pSource[1] = L.xor(R);
|
||||
});
|
||||
pSource.reverse();
|
||||
out = pSource[1].shiftLeft(32).and(-4294967296).or(pSource[0].and(0xffffffff));
|
||||
out = bitTransform(arrayIP_1, 64, out);
|
||||
return out;
|
||||
};
|
||||
|
||||
const subKeys = (l, longs, n) => {
|
||||
// long, long[], int
|
||||
let l2 = bitTransform(arrayPC_1, 56, l);
|
||||
range(16).forEach((i) => {
|
||||
l2 = l2
|
||||
.and(arrayLsMask[arrayLs[i]])
|
||||
.shiftLeft(28 - arrayLs[i])
|
||||
.or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]));
|
||||
longs[i] = bitTransform(arrayPC_2, 64, l2);
|
||||
});
|
||||
if (n === 1) {
|
||||
range(8).forEach((j) => {
|
||||
[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const crypt = (msg, key, mode) => {
|
||||
// 处理密钥块
|
||||
let l = Long(0);
|
||||
range(8).forEach((i) => {
|
||||
l = Long(key[i])
|
||||
.shiftLeft(i * 8)
|
||||
.or(l);
|
||||
});
|
||||
|
||||
const j = Math.floor(msg.length / 8);
|
||||
// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
|
||||
|
||||
const arrLong1 = range(16).map(() => Long(0));
|
||||
subKeys(l, arrLong1, mode);
|
||||
|
||||
// arrLong2 存放的是前部分的明文
|
||||
const arrLong2 = range(j).map(() => Long(0));
|
||||
|
||||
range(j).forEach((m) => {
|
||||
range(8).forEach((n) => {
|
||||
arrLong2[m] = Long(msg[n + m * 8])
|
||||
.shiftLeft(n * 8)
|
||||
.or(arrLong2[m]);
|
||||
});
|
||||
});
|
||||
|
||||
// 用于存放密文
|
||||
const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0));
|
||||
|
||||
// 计算前部的数据块(除了最后一部分)
|
||||
range(j).forEach((i1) => {
|
||||
arrLong3[i1] = DES64(arrLong1, arrLong2[i1]);
|
||||
});
|
||||
|
||||
// 保存多出来的字节
|
||||
const arrByte1 = msg.slice(j * 8);
|
||||
let l2 = Long(0);
|
||||
|
||||
range(msg.length % 8).forEach((i1) => {
|
||||
l2 = Long(arrByte1[i1])
|
||||
.shiftLeft(i1 * 8)
|
||||
.or(l2);
|
||||
});
|
||||
|
||||
// 计算多出的那一位(最后一位)
|
||||
if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2); // 解密不需要
|
||||
|
||||
// 将密文转为字节型
|
||||
const arrByte2 = range(8 * arrLong3.length).map(() => 0);
|
||||
let i4 = 0;
|
||||
arrLong3.forEach((l3) => {
|
||||
range(8).forEach((i6) => {
|
||||
arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low;
|
||||
i4 += 1;
|
||||
});
|
||||
});
|
||||
return Buffer.from(arrByte2);
|
||||
};
|
||||
|
||||
const SECRET_KEY = Buffer.from("ylzsxkwm");
|
||||
export const encrypt = (msg) => crypt(msg, SECRET_KEY, 0);
|
||||
export const decrypt = (msg) => crypt(msg, SECRET_KEY, 1);
|
||||
export const encryptQuery = (query) => encrypt(Buffer.from(query)).toString("base64");
|
||||
151
electron/main/utils/readDirAsync.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { parseFile } from "music-metadata";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 从指定文件夹中递归读取音乐文件,并将它们以数组形式返回
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} fileLimit - 返回的音乐文件数量限制
|
||||
* @returns {Array} - 包含音乐文件信息的数组
|
||||
*/
|
||||
export const readDirAsync = async (directoryPath, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
// 递归读取文件夹中的项目
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
// 若为音频文件
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
// 音乐文件信息
|
||||
const fileInfo = {
|
||||
id: generateId(itemPath),
|
||||
name: common.title,
|
||||
path: itemPath,
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2),
|
||||
time: stats.mtime?.getTime(),
|
||||
artists: common.artists?.[0],
|
||||
album: common.album,
|
||||
alia: common.comment?.[0],
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
result.push(fileInfo);
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
// 若为文件夹
|
||||
if (stats.isDirectory()) {
|
||||
// 读取子文件夹中的项目
|
||||
const subItems = await fs.readdir(itemPath);
|
||||
for (const subItem of subItems) {
|
||||
await readItem(path.join(item, subItem));
|
||||
}
|
||||
}
|
||||
};
|
||||
// 从根目录开始读取
|
||||
await readItem("");
|
||||
// 返回不超过上限的音乐文件列表
|
||||
return result.slice(0, fileLimit);
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归地读取文件夹内容,包括文件和子文件夹的信息
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} depth - 递归深度(默认为 -1,无限递归)
|
||||
* @param {number} fileLimit - 文件总数
|
||||
* @returns {Promise<Array>} 包含文件和子文件夹信息的树形数组
|
||||
*/
|
||||
export const readDirTreeAsync = async (directoryPath, depth = -1, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
|
||||
const fileInfo = {
|
||||
id: generateId(item),
|
||||
name: item,
|
||||
path: itemPath,
|
||||
type: stats.isFile() ? "song" : "dir",
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2), // 文件大小
|
||||
modified: stats.mtime, // 修改日期
|
||||
};
|
||||
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
fileInfo.metadata = {
|
||||
name: common.title,
|
||||
artists: common.artists,
|
||||
album: common.album,
|
||||
date: common.date,
|
||||
alia: common.comment?.[0],
|
||||
year: common.year,
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.isDirectory() && (depth === -1 || depth > 0)) {
|
||||
// 如果是文件夹且未达到递归深度限制,且文件数量未达到上限,则递归读取文件夹内容
|
||||
if (fileInfo.type === "dir" && result.length < fileLimit) {
|
||||
fileInfo.children = await readDirAsync(
|
||||
itemPath,
|
||||
depth === -1 ? -1 : depth - 1,
|
||||
fileLimit - result.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.push(fileInfo);
|
||||
};
|
||||
|
||||
const items = await fs.readdir(directoryPath);
|
||||
await Promise.all(items.map(readItem));
|
||||
|
||||
return result.slice(0, fileLimit); // 返回不超过上限的文件列表
|
||||
};
|
||||
|
||||
/**
|
||||
* 歌曲时长时间戳转换
|
||||
* @param {number} mss 毫秒数
|
||||
* @returns {string} 格式为 "mm:ss" 的字符串
|
||||
*/
|
||||
const formatDuration = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
|
||||
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`;
|
||||
return `${formattedMinutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断文件是否为音频文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} - 是否为音频文件
|
||||
*/
|
||||
const isAudioFile = (filePath) => {
|
||||
const audioExtensions = [".flac", ".mp3"];
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
return audioExtensions.includes(extension);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文件名生成数字 ID
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {number} - 生成的数字ID
|
||||
*/
|
||||
const generateId = (fileName) => {
|
||||
// 将文件名转换为哈希值
|
||||
let hash = 0;
|
||||
for (let i = 0; i < fileName.length; i++) {
|
||||
hash = (hash << 5) - hash + fileName.charCodeAt(i);
|
||||
}
|
||||
// 将哈希值转换为正整数
|
||||
const numericId = Math.abs(hash % 10000000000);
|
||||
return numericId;
|
||||
};
|
||||
15
electron/preload/index.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import { contextBridge } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
// 使用 contextBridge 暴露 electronAPI 到渲染进程的全局对象中
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象
|
||||
window.electron = electronAPI;
|
||||
}
|
||||
51
index.html
@@ -1,30 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/imgs/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/imgs/icons/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/imgs/icons/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<link rel="mask-icon" href="/imgs/icons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/images/logo/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
<title>SPlayer</title>
|
||||
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" />
|
||||
<meta name="description" content="一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" />
|
||||
<!-- HarmonyOS Sans -->
|
||||
<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" />
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?c6579e9a33cbc5260fc90231678556ec";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
nginx.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 7899;
|
||||
listen [::]:7899;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_buffers 16 32k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 128k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
91
package.json
@@ -1,38 +1,77 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"version": "2.0.8",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"npm": ">=9.6.7",
|
||||
"pnpm": ">=8.14.0"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
"build:mac": "npm run build && electron-builder --mac --config",
|
||||
"build:linux": "npm run build && electron-builder --linux --config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"artplayer": "^4.5.12",
|
||||
"axios": "^1.2.0",
|
||||
"pinia": "^2.0.26",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
"plyr": "^3.7.3",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"sass": "^1.56.1",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.19.8",
|
||||
"axios": "^1.7.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"express": "^4.19.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"font-list": "^1.5.1",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.14.0",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.3.2",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"naive-ui": "^2.34.2",
|
||||
"unplugin-auto-import": "^0.12.0",
|
||||
"unplugin-vue-components": "^0.22.11",
|
||||
"vite": "^3.2.4",
|
||||
"vite-plugin-pwa": "^0.14.1"
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.13.0",
|
||||
"electron": "^28.3.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-vite": "^2.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"naive-ui": "^2.38.2",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.2",
|
||||
"terser": "^5.31.0",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"vue": "3.4.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10795
pnpm-lock.yaml
generated
BIN
public/favicon.png
Normal file
BIN
public/font/HarmonyOS_Sans_SC.woff2
Normal file
BIN
public/font/HarmonyOS_Sans_SC_Bold.woff2
Normal file
13
public/font/font.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@font-face {
|
||||
font-family: "HarmonyOS Sans";
|
||||
src: url("./HarmonyOS_Sans_SC.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "HarmonyOS Sans";
|
||||
src: url("./HarmonyOS_Sans_SC_Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1669874203265" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2683" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 160c70.4 0 128 57.6 128 128s-57.6 128-128 128-128-57.6-128-128 57.6-128 128-128z m236.8 507.733333c-23.466667 32-117.333333 100.266667-236.8 100.266667s-213.333333-68.266667-236.8-100.266667c-8.533333-10.666667-10.666667-21.333333-8.533333-32 29.866667-110.933333 130.133333-187.733333 245.333333-187.733333s215.466667 76.8 245.333333 187.733333c2.133333 10.666667 0 21.333333-8.533333 32z" p-id="2684" fill="#8a8a8a"></path></svg>
|
||||
|
Before Width: | Height: | Size: 948 B |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 217 KiB |
BIN
public/imgs/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/imgs/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/imgs/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/imgs/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
public/imgs/icons/favicon-192x192.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/imgs/icons/favicon-256x256.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/imgs/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/imgs/icons/favicon-512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/imgs/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/imgs/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/imgs/icons/favicon.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
public/imgs/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/imgs/icons/next-dark.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/imgs/icons/next-light.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/imgs/icons/open-dark.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/imgs/icons/open-light.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/imgs/icons/pause-dark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/imgs/icons/pause-light.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/imgs/icons/play-dark.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/imgs/icons/play-light.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/imgs/icons/power-dark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/imgs/icons/power-light.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/imgs/icons/prev-dark.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/imgs/icons/prev-light.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
25
public/imgs/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2400 4444 c-14 -2 -63 -8 -110 -14 -407 -50 -737 -184 -1077 -436
|
||||
-124 -93 -348 -309 -406 -394 -100 -144 -190 -286 -223 -352 -101 -200 -199
|
||||
-506 -226 -707 -17 -127 -22 -436 -8 -546 40 -338 161 -687 327 -942 138 -213
|
||||
226 -319 372 -450 237 -211 471 -353 761 -460 147 -54 156 -57 330 -88 383
|
||||
-71 516 -67 965 25 76 16 288 91 370 132 28 13 66 31 85 38 39 16 158 88 270
|
||||
164 242 164 474 410 628 668 254 422 362 920 308 1408 -16 139 -23 172 -73
|
||||
344 -80 273 -188 496 -346 711 -104 143 -302 348 -404 421 l-48 34 -192 0
|
||||
c-184 0 -193 -1 -203 -21 -8 -14 -11 -271 -10 -882 1 -474 -1 -896 -5 -937
|
||||
-29 -306 -228 -596 -507 -737 -207 -104 -426 -126 -663 -66 -168 43 -300 125
|
||||
-453 283 -118 122 -203 310 -223 495 -10 86 -3 271 11 320 41 135 58 180 95
|
||||
248 103 193 330 376 545 441 157 47 364 49 513 5 38 -11 78 -19 90 -17 l22 3
|
||||
-3 635 c-2 349 -7 638 -10 642 -22 22 -413 47 -502 32z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/imgs/icons/setting-dark.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/imgs/icons/setting-light.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/imgs/pic/album.jpg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/imgs/pic/artist.jpg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/imgs/pic/avatar.png
Normal file
|
After Width: | Height: | Size: 878 B |
BIN
public/imgs/pic/cover-2.jpg
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/imgs/pic/cover.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/imgs/pic/default.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/imgs/pic/like.jpg
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/imgs/pic/pl-1.jpg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/imgs/pic/pl-2.jpg
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/imgs/pic/pl-3.jpg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/imgs/pic/pl-4.jpg
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
public/imgs/pic/radar-private.jpg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/imgs/pic/radar-treasure.jpg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/imgs/pic/record.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/imgs/pic/song.jpg
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/imgs/pic/video.jpg
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/imgs/pic/vip.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
screenshots/SPlayer - 主页面.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 344 KiB |
BIN
screenshots/SPlayer - 发现页面.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 338 KiB |
BIN
screenshots/SPlayer - 播放页面.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 324 KiB |