mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 19:37:35 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 |
17
.github/ISSUE_TEMPLATE/add.yml
vendored
17
.github/ISSUE_TEMPLATE/add.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 添加功能
|
||||
description: 请填写希望添加的功能的具体信息
|
||||
title: 【添加功能】请填写标题
|
||||
labels: [add]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "希望添加什么功能?"
|
||||
placeholder: "请填写功能名称"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请详细描述希望添加的功能的具体信息"
|
||||
5
.github/ISSUE_TEMPLATE/bug.yml
vendored
5
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 【遇到问题】请填写标题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
@@ -31,4 +31,5 @@ body:
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的"
|
||||
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
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, 新的功能建议和提问答疑请到讨论区发起
|
||||
|
||||
21
README.md
21
README.md
@@ -1,10 +1,20 @@
|
||||
<div align="center">
|
||||
<!-- <div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/icons/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
<br /> -->
|
||||
|
||||
# SPlayer
|
||||
|
||||
> 一个简约的音乐播放器
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 由于 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 项目已停止维护,由于接口状态的不可确定性,将无法保障功能的正常使用,本项目将会停止新功能的开发,进入无限期停更状态
|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
@@ -23,7 +33,7 @@
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
|
||||
|
||||
|
||||
- 欢迎各位大佬 `Star` 😍
|
||||
|
||||
## 👀 Demo
|
||||
@@ -137,8 +147,11 @@ docker-compose up -d
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
# 拉取
|
||||
# 从 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
|
||||
```
|
||||
|
||||
@@ -42,7 +42,7 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.js"),
|
||||
index: resolve(__dirname, "electron/preload/index.mjs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -121,8 +121,8 @@ class MainProcess {
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
// devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import NodeID3 from "node-id3";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
@@ -193,37 +193,42 @@ const mainIpcMain = (win) => {
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
||||
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)) {
|
||||
const songData = JSON.parse(song);
|
||||
console.info("开始下载:", songData, data);
|
||||
console.info("开始下载:", name, url);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, data.url, {
|
||||
const songDownload = await download(win, url, {
|
||||
directory: path,
|
||||
filename: `${songName}.${songType}`,
|
||||
filename: `${name}.${type}`,
|
||||
});
|
||||
// 若不为 mp3,则不进行元信息写入
|
||||
if (songType !== "mp3") return true;
|
||||
// 若关闭,则不进行元信息写入
|
||||
if (!downloadMeta) return true;
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, songData.cover, {
|
||||
const coverDownload = await download(win, data.cover, {
|
||||
directory: path,
|
||||
filename: `${songName}.jpg`,
|
||||
filename: `${name}.jpg`,
|
||||
});
|
||||
// 生成歌曲文件的元数据
|
||||
const songTag = {
|
||||
title: songData.name,
|
||||
artist: Array.isArray(songData.artists)
|
||||
? songData.artists.map((ar) => ar.name).join(" / ")
|
||||
: songData.artists || "未知歌手",
|
||||
album: songData.album?.name || songData.album,
|
||||
image: coverDownload.getSavePath(),
|
||||
};
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
const isSuccess = NodeID3.write(songTag, songDownload.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 isSuccess;
|
||||
return true;
|
||||
} else {
|
||||
console.log(`目录不存在:${path}`);
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
import netEaseApi from "NeteaseCloudMusicApi";
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { dialog, shell } from "electron";
|
||||
import { dialog } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import pkg from "electron-updater";
|
||||
|
||||
@@ -10,23 +10,56 @@ const hasNewVersion = (info) => {
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否前往 GitHub 下载新版本安装包?",
|
||||
buttons: ["前往", "取消"],
|
||||
detail: "是否立即下载并安装新版本?",
|
||||
buttons: ["立即下载", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
shell.openExternal("https://github.com/imsyy/SPlayer/releases");
|
||||
// 触发手动下载
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
if (is.dev) return false;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
// 监听下载进度事件
|
||||
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();
|
||||
};
|
||||
|
||||
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.4",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
@@ -14,6 +14,7 @@
|
||||
"npm": ">=9.6.7",
|
||||
"pnpm": ">=8.14.0"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
@@ -29,47 +30,47 @@
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.14.0",
|
||||
"axios": "^1.6.5",
|
||||
"NeteaseCloudMusicApi": "^4.15.3",
|
||||
"axios": "^1.6.7",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-updater": "^6.1.8",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.14.0",
|
||||
"node-id3": "^0.2.6",
|
||||
"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-router": "^4.2.5",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.6.1",
|
||||
"@vitejs/plugin-vue": "^5.0.2",
|
||||
"@rushstack/eslint-patch": "^1.7.2",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.12.0",
|
||||
"electron": "^28.1.2",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-log": "^5.0.3",
|
||||
"electron": "^28.2.4",
|
||||
"electron-builder": "^24.12.0",
|
||||
"electron-log": "^5.1.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"naive-ui": "^2.37.3",
|
||||
"prettier": "^3.1.1",
|
||||
"sass": "^1.69.7",
|
||||
"terser": "^5.26.0",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.1",
|
||||
"terser": "^5.27.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vue": "3.4.4"
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"vue": "3.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
2168
pnpm-lock.yaml
generated
2168
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
screenshots/SPlayer.jpg
Normal file
BIN
screenshots/SPlayer.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 524 KiB |
@@ -32,7 +32,7 @@
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}"
|
||||
:class="music.getPlaySongData?.id === item?.id ? 'songs play' : 'songs'"
|
||||
:class="Number(music.getPlaySongData?.id) === Number(item?.id) ? 'songs play' : 'songs'"
|
||||
hoverable
|
||||
@click="checkCanClick(data, item, songsIndex + index)"
|
||||
@dblclick.stop="playSong(data, item, songsIndex + index)"
|
||||
@@ -190,7 +190,7 @@
|
||||
</div>
|
||||
<!-- 更新日期 -->
|
||||
<n-text v-if="type === 'dj' && item.updateTime" class="update hidden" depth="3">
|
||||
{{ getTimestampTime(item.updateTime, false) }}
|
||||
{{ djFormatDate(item.updateTime) }}
|
||||
</n-text>
|
||||
<!-- 播放量 -->
|
||||
<n-text v-if="type === 'dj' && item.playCount" class="count hidden" depth="3">
|
||||
@@ -273,7 +273,7 @@ import { setCloudDel } from "@/api/cloud";
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { siteData, siteSettings, musicData, siteStatus } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, addSongToNext } from "@/utils/Player";
|
||||
import { getTimestampTime } from "@/utils/timeTools";
|
||||
import { djFormatDate } from "@/utils/timeTools";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
@@ -439,6 +439,7 @@ const delCloudSong = (data, song, index) => {
|
||||
|
||||
// 歌单歌曲删除
|
||||
const deletePlaylistSong = (pid, song, data, index) => {
|
||||
if (!pid || !song) return $message.error("无法正确定位到歌单,请重试");
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
|
||||
@@ -698,7 +699,7 @@ onBeforeUnmount(() => {
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.play {
|
||||
.num {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
playMode = 'song';
|
||||
playMode = 'normal';
|
||||
addSongToNext(songData);
|
||||
}
|
||||
"
|
||||
@@ -130,7 +130,7 @@
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('deletePlaylistSong', playlistData, songData, songIndex);
|
||||
emit('deletePlaylistSong', songSourceId, songData, playlistData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
|
||||
@@ -147,7 +147,7 @@ const openDropdown = (e, data, song, index, sourceId, type) => {
|
||||
show: isSong && playMode.value !== "dj" && music.getPlaySongData?.id !== song.id && !isFm,
|
||||
props: {
|
||||
onClick: () => {
|
||||
playMode.value = "song";
|
||||
playMode.value = "normal";
|
||||
addSongToNext(song);
|
||||
},
|
||||
},
|
||||
@@ -240,7 +240,7 @@ const openDropdown = (e, data, song, index, sourceId, type) => {
|
||||
show: !isCloud && isUserPlaylist,
|
||||
props: {
|
||||
onClick: () => {
|
||||
emit("deletePlaylistSong", data, song, index);
|
||||
emit("deletePlaylistSong", sourceId, song, data, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
|
||||
@@ -43,8 +43,9 @@
|
||||
<n-button
|
||||
:disabled="!downloadChoose"
|
||||
:loading="downloadStatus"
|
||||
:focusable="false"
|
||||
type="primary"
|
||||
@click="toSongDownload(songData, downloadChoose)"
|
||||
@click="toSongDownload(songData, lyricData, downloadChoose)"
|
||||
>
|
||||
下载
|
||||
</n-button>
|
||||
@@ -58,7 +59,7 @@ import { storeToRefs } from "pinia";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getSongDetail, getSongDownload } from "@/api/song";
|
||||
import { getSongDetail, getSongDownload, getSongLyric } from "@/api/song";
|
||||
import { downloadFile, checkPlatform } from "@/utils/helper";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
@@ -66,11 +67,12 @@ const router = useRouter();
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { downloadPath } = storeToRefs(settings);
|
||||
const { downloadPath, downloadMeta, downloadCover, downloadLyrics } = storeToRefs(settings);
|
||||
|
||||
// 歌曲下载数据
|
||||
const songId = ref(null);
|
||||
const songData = ref(null);
|
||||
const lyricData = ref(null);
|
||||
const downloadStatus = ref(false);
|
||||
const downloadSongShow = ref(false);
|
||||
const downloadChoose = ref(null);
|
||||
@@ -79,11 +81,13 @@ const downloadLevel = ref(null);
|
||||
// 获取歌曲详情
|
||||
const getMusicDetailData = async (id) => {
|
||||
try {
|
||||
const result = await getSongDetail(id);
|
||||
const songResult = await getSongDetail(id);
|
||||
const lyricResult = await getSongLyric(id);
|
||||
// 获取歌曲详情
|
||||
songData.value = formatData(result?.songs?.[0], "song")[0];
|
||||
songData.value = formatData(songResult?.songs?.[0], "song")[0];
|
||||
lyricData.value = lyricResult?.lrc?.lyric || null;
|
||||
// 生成音质列表
|
||||
generateLists(result);
|
||||
generateLists(songResult);
|
||||
} catch (error) {
|
||||
closeDownloadModal();
|
||||
console.error("歌曲信息获取失败:", error);
|
||||
@@ -91,26 +95,42 @@ const getMusicDetailData = async (id) => {
|
||||
};
|
||||
|
||||
// 歌曲下载
|
||||
const toSongDownload = async (song, br) => {
|
||||
console.log(song, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value && checkPlatform.electron()) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
const toSongDownload = async (song, lyric, br) => {
|
||||
try {
|
||||
console.log(song, lyric, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value && checkPlatform.electron()) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
if (!result.data?.url) {
|
||||
downloadStatus.value = false;
|
||||
return $message.error("下载失败,请重试");
|
||||
}
|
||||
// 获取下载结果
|
||||
const isDownloaded = await downloadFile(result.data, song, lyric, {
|
||||
path: downloadPath.value,
|
||||
downloadMeta: downloadMeta.value,
|
||||
downloadCover: downloadCover.value,
|
||||
downloadLyrics: downloadLyrics.value,
|
||||
});
|
||||
}
|
||||
const isDownloaded = await downloadFile(result.data, song, downloadPath.value);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
console.log(lyric);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("歌曲下载出错:", error);
|
||||
$message.error("歌曲下载失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -104,36 +104,39 @@ const getCaptcha = (phone) => {
|
||||
|
||||
// 手机号登录
|
||||
const phoneLogin = (e) => {
|
||||
e.preventDefault();
|
||||
phoneFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const verifyRes = await verifyCaptcha(
|
||||
phoneFormData._value.phone,
|
||||
phoneFormData._value.captcha,
|
||||
);
|
||||
console.log(verifyRes);
|
||||
if (verifyRes.code == 200) {
|
||||
const result = await toLogin(phoneFormData._value.phone, phoneFormData._value.captcha);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 去除 HTTPOnly
|
||||
result.cookie = result.cookie.replaceAll(" HTTPOnly", "");
|
||||
// 是否含有 MUSIC_U
|
||||
if (result.cookie && result.cookie.includes("MUSIC_U")) {
|
||||
// 储存登录信息
|
||||
emit("setLoginData", result);
|
||||
} else {
|
||||
$message.error("登录出错,请重试");
|
||||
try {
|
||||
e.preventDefault();
|
||||
phoneFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const verifyRes = await verifyCaptcha(
|
||||
phoneFormData._value.phone,
|
||||
phoneFormData._value.captcha,
|
||||
);
|
||||
console.log(verifyRes);
|
||||
if (verifyRes.code == 200) {
|
||||
const result = await toLogin(phoneFormData._value.phone, phoneFormData._value.captcha);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 去除 HTTPOnly
|
||||
result.cookie = result.cookie.replaceAll(" HTTPOnly", "");
|
||||
// 是否含有 MUSIC_U
|
||||
if (result.cookie && result.cookie.includes("MUSIC_U")) {
|
||||
// 储存登录信息
|
||||
emit("setLoginData", result);
|
||||
} else {
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
phoneFormData.value.captcha = null;
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
phoneFormData.value.captcha = null;
|
||||
console.error("登录出错:", error);
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- 用户信息 -->
|
||||
<userData />
|
||||
<!-- TitleBar -->
|
||||
<TitleBar v-if="checkPlatform.electron()" />
|
||||
<TitleBar v-if="isElectron" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -122,6 +122,11 @@ const openGithub = () => {
|
||||
window.open(packageJson.github);
|
||||
};
|
||||
|
||||
// 是否为 Electron
|
||||
const isElectron = computed(() => {
|
||||
return checkPlatform.electron() || typeof electron !== "undefined";
|
||||
});
|
||||
|
||||
// 主菜单渲染
|
||||
const mainMenuShow = ref(false);
|
||||
const mainMenuOptions = computed(() => [
|
||||
|
||||
@@ -131,6 +131,46 @@
|
||||
>
|
||||
<SvgIcon icon="comment-text" />
|
||||
</n-icon>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
class="volume hidden"
|
||||
size="22"
|
||||
@click.stop="setVolumeMute"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon v-else-if="playVolume > 0 && playVolume < 0.4" icon="volume-mute-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
:style="{
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
class="slider-content hidden"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放模式 -->
|
||||
<n-icon v-if="playMode === 'normal'" class="hidden" size="22" @click.stop="togglePlayMode">
|
||||
<SvgIcon
|
||||
@@ -163,7 +203,14 @@
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus, siteData } from "@/stores";
|
||||
import { useRouter } from "vue-router";
|
||||
import { playOrPause, fadePlayOrPause, setSeek, changePlayIndex } from "@/utils/Player";
|
||||
import {
|
||||
playOrPause,
|
||||
fadePlayOrPause,
|
||||
setSeek,
|
||||
changePlayIndex,
|
||||
setVolume,
|
||||
setVolumeMute,
|
||||
} from "@/utils/Player";
|
||||
import debounce from "@/utils/debounce";
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
@@ -185,6 +232,8 @@ const {
|
||||
playMode,
|
||||
playSongMode,
|
||||
playHeartbeatMode,
|
||||
playVolume,
|
||||
coverTheme,
|
||||
} = storeToRefs(status);
|
||||
|
||||
// 子组件
|
||||
@@ -234,6 +283,24 @@ const togglePlayMode = () => {
|
||||
playSongMode.value = modeMap[playSongMode.value] || "normal";
|
||||
};
|
||||
|
||||
// 音量条鼠标滚动
|
||||
const changeVolume = (e) => {
|
||||
const deltaY = e.deltaY;
|
||||
if (deltaY > 0) {
|
||||
// 向下滚动
|
||||
if (playVolume.value >= 0) {
|
||||
playVolume.value = Math.max(playVolume.value - 0.05, 0);
|
||||
}
|
||||
} else if (deltaY < 0) {
|
||||
// 向上滚动
|
||||
if (playVolume.value < 1) {
|
||||
playVolume.value = Math.min(playVolume.value + 0.05, 1);
|
||||
}
|
||||
}
|
||||
// 更新音量
|
||||
setVolume(playVolume.value);
|
||||
};
|
||||
|
||||
// 控制面板移入
|
||||
const controlEnter = () => {
|
||||
if (controlTimeOut.value) clearTimeout(controlTimeOut.value);
|
||||
@@ -398,4 +465,23 @@ const controlMove = (e) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 音量控制
|
||||
.slider-content {
|
||||
padding: 10px 0px;
|
||||
width: 50px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--cover-second-color);
|
||||
.n-slider {
|
||||
--n-fill-color: var(--cover-main-color);
|
||||
--n-fill-color-hover: var(--cover-main-color);
|
||||
--n-handle-color: var(--cover-main-color);
|
||||
}
|
||||
.slider-num {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,11 +19,20 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
playSongSource: 0,
|
||||
// 当前歌曲歌词数据
|
||||
playSongLyric: {
|
||||
lrc: [],
|
||||
yrc: [],
|
||||
hasTran: false,
|
||||
hasRoma: false,
|
||||
// 是否具有普通翻译
|
||||
hasLrcTran: false,
|
||||
// 是否具有普通音译
|
||||
hasLrcRoma: false,
|
||||
// 是否具有逐字歌词
|
||||
hasYrc: false,
|
||||
// 是否具有逐字翻译
|
||||
hasYrcTran: false,
|
||||
// 是否具有逐字音译
|
||||
hasYrcRoma: false,
|
||||
// 普通歌词数组
|
||||
lrc: [],
|
||||
// 逐字歌词数据
|
||||
yrc: [],
|
||||
},
|
||||
// 本地歌曲目录
|
||||
localSongPath: [],
|
||||
|
||||
@@ -48,6 +48,9 @@ const useSiteSettingsStore = defineStore("siteSettings", {
|
||||
showRoma: true, // 是否显示歌词音译
|
||||
// 下载部分
|
||||
downloadPath: null, // 默认下载路径
|
||||
downloadMeta: true, // 同时下载元信息
|
||||
downloadCover: true, // 同时下载封面
|
||||
downloadLyrics: true, // 同时下载歌词
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { decode as base642Buffer } from "@/utils/base64";
|
||||
import { getSongPlayTime } from "@/utils/timeTools";
|
||||
import { getCoverGradient } from "@/utils/cover-color";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import parseLyric from "@/utils/parseLyric";
|
||||
import { parseLyric, parseLocalLrc } from "@/utils/parseLyric";
|
||||
|
||||
// 全局播放器
|
||||
let player;
|
||||
@@ -42,10 +42,9 @@ export const initPlayer = async (playNow = false) => {
|
||||
const { playList } = music;
|
||||
// 当前播放歌曲数据
|
||||
const playSongData = music.getPlaySongData;
|
||||
// 若为电台则更改 id
|
||||
playSongData.id = playMode === "dj" ? playSongData.mainTrackId : playSongData.id;
|
||||
// 是否为本地歌曲
|
||||
const isLocalSong = playSongData?.path ? true : false;
|
||||
console.log("当前为本地歌曲");
|
||||
// 获取封面
|
||||
if (isLocalSong) {
|
||||
music.playSongData.localCover = await getLocalCoverData(playSongData?.path);
|
||||
@@ -597,17 +596,20 @@ const getSongLyricData = async (islocal, data) => {
|
||||
const music = musicData();
|
||||
const setDefaults = () => {
|
||||
music.playSongLyric = {
|
||||
hasLrcTran: false,
|
||||
hasLrcRoma: false,
|
||||
hasYrc: false,
|
||||
hasYrcTran: false,
|
||||
hasYrcRoma: false,
|
||||
lrc: [],
|
||||
yrc: [],
|
||||
hasTran: false,
|
||||
hasRoma: false,
|
||||
hasYrc: false,
|
||||
};
|
||||
};
|
||||
if (islocal) {
|
||||
const lyricData = await electron.ipcRenderer.invoke("getMusicLyric", data?.path);
|
||||
if (lyricData) {
|
||||
music.playSongLyric = parseLyric({ lrc: { lyric: lyricData } });
|
||||
const result = parseLocalLrc(lyricData);
|
||||
music.playSongLyric = result ? (music.playSongLyric = result) : setDefaults();
|
||||
} else {
|
||||
console.log("该歌曲暂无歌词");
|
||||
setDefaults();
|
||||
@@ -616,7 +618,8 @@ const getSongLyricData = async (islocal, data) => {
|
||||
const lyricResponse = await getSongLyric(data?.id);
|
||||
const lyricData = lyricResponse?.lrc;
|
||||
if (lyricData) {
|
||||
music.playSongLyric = parseLyric(lyricResponse);
|
||||
const result = parseLyric(lyricResponse);
|
||||
result ? (music.playSongLyric = result) : setDefaults();
|
||||
} else {
|
||||
console.log("该歌曲暂无歌词");
|
||||
setDefaults();
|
||||
@@ -812,6 +815,10 @@ export const playAllSongs = async (playlist, mode = "normal") => {
|
||||
// 播放
|
||||
fadePlayOrPause();
|
||||
}
|
||||
// 获取封面
|
||||
if (music.getPlaySongData?.path) {
|
||||
music.playSongData.localCover = await getLocalCoverData(music.getPlaySongData?.path);
|
||||
}
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
} catch (error) {
|
||||
console.error("播放全部歌曲出错:", error);
|
||||
|
||||
@@ -112,8 +112,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
// dj
|
||||
case "dj":
|
||||
return {
|
||||
id: v.id || v.vid,
|
||||
mainTrackId: v.mainTrackId,
|
||||
id: v.mainTrackId || v.id || v.vid,
|
||||
name: v.name,
|
||||
creator: v.dj,
|
||||
count: v.programCount,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// BlobUrl
|
||||
let lastSongBlobUrl = null;
|
||||
let lastCoverBlobUrl = null;
|
||||
let lastDownloadBlobUrl = null;
|
||||
|
||||
/**
|
||||
* 判断当前运行环境
|
||||
*/
|
||||
@@ -59,27 +64,15 @@ export const getCacheData = async (key, time, request, params) => {
|
||||
*/
|
||||
export const getLocalCoverData = async (path, isAlbum = false) => {
|
||||
try {
|
||||
let blobUrl = null;
|
||||
// 清理过期的 Blob 链接
|
||||
if (lastCoverBlobUrl) URL.revokeObjectURL(lastCoverBlobUrl);
|
||||
const coverData = await electron.ipcRenderer.invoke("getMusicCover", path);
|
||||
if (coverData) {
|
||||
// 将 Uint8Array 数据转换为 Blob
|
||||
const blob = new Blob([coverData.coverData], { type: `image/${coverData.coverFormat}` });
|
||||
// 生成Blob URL
|
||||
blobUrl = URL.createObjectURL(blob);
|
||||
// 检查当前path是否与上次不一致
|
||||
const previousPath = sessionStorage.getItem("localCoverPath");
|
||||
if (previousPath && previousPath !== path) {
|
||||
// 清除上次的内容
|
||||
const previousBlobUrl = sessionStorage.getItem("localCoverBlobUrl");
|
||||
if (previousBlobUrl) {
|
||||
URL.revokeObjectURL(previousBlobUrl);
|
||||
sessionStorage.removeItem("localCoverBlobUrl");
|
||||
}
|
||||
}
|
||||
// 存储当前path和Blob URL
|
||||
sessionStorage.setItem("localCoverPath", path);
|
||||
sessionStorage.setItem("localCoverBlobUrl", blobUrl);
|
||||
return blobUrl;
|
||||
lastCoverBlobUrl = URL.createObjectURL(blob);
|
||||
return lastCoverBlobUrl;
|
||||
} else {
|
||||
// 如果没有封面数据
|
||||
return `/images/pic/${isAlbum ? "album" : "song"}.jpg?assest`;
|
||||
@@ -299,35 +292,40 @@ export const generateId = (fileName) => {
|
||||
* @param {String} songName - 歌曲名称
|
||||
* @returns {number} - 生成的数字ID
|
||||
*/
|
||||
export const downloadFile = async (data, song, path = null) => {
|
||||
export const downloadFile = async (data, song, lyric, options) => {
|
||||
try {
|
||||
const isElectron = checkPlatform.electron();
|
||||
const songType = data.type.toLowerCase();
|
||||
// 歌曲名称
|
||||
const songName =
|
||||
song.name +
|
||||
" - " +
|
||||
"-" +
|
||||
(Array.isArray(song.artists)
|
||||
? song.artists.map((ar) => ar.name).join(" / ")
|
||||
? song.artists.map((ar) => ar.name).join("&")
|
||||
: song.artists || "未知歌手");
|
||||
if (isElectron && path) {
|
||||
console.log("开始下载:", data, song, songName, songType, path);
|
||||
if (isElectron && options.path) {
|
||||
console.log("开始下载:", data, song, songName, songType, options.path);
|
||||
return await electron.ipcRenderer.invoke(
|
||||
"downloadFile",
|
||||
data,
|
||||
JSON.stringify(song),
|
||||
songName,
|
||||
songType,
|
||||
path,
|
||||
JSON.stringify({
|
||||
url: data?.url,
|
||||
data: song,
|
||||
lyric: lyric,
|
||||
name: songName,
|
||||
type: songType,
|
||||
}),
|
||||
JSON.stringify(options),
|
||||
);
|
||||
} else {
|
||||
const songRes = await fetch(data.url.replace(/^http:/, "https:"));
|
||||
// 清理过期的 Blob 链接
|
||||
if (lastDownloadBlobUrl) URL.revokeObjectURL(lastDownloadBlobUrl);
|
||||
const songRes = await fetch(data?.url.replace(/^http:/, "https:"));
|
||||
if (!songRes.ok) throw new Error("下载出错,请重试");
|
||||
const blob = await songRes.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
lastDownloadBlobUrl = URL.createObjectURL(blob);
|
||||
// 下载数据
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.href = lastDownloadBlobUrl;
|
||||
a.download = `${songName}.${songType}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
@@ -359,12 +357,10 @@ export const formatBytes = (bytes, decimals = 2) => {
|
||||
* 获取音频文件的 Blob 链接
|
||||
* @param {string} url - 音频文件的网络链接
|
||||
*/
|
||||
// 上次生成的 BlobUrl
|
||||
let lastBlobUrl = null;
|
||||
export const getBlobUrlFromUrl = async (url) => {
|
||||
try {
|
||||
// 清理过期的 Blob 链接
|
||||
if (lastBlobUrl) URL.revokeObjectURL(lastBlobUrl);
|
||||
if (lastSongBlobUrl) URL.revokeObjectURL(lastSongBlobUrl);
|
||||
// 是否为网络链接
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("blob:")) {
|
||||
return url;
|
||||
@@ -377,8 +373,8 @@ export const getBlobUrlFromUrl = async (url) => {
|
||||
}
|
||||
const blob = await response.blob();
|
||||
// 转换为本地 Blob 链接
|
||||
lastBlobUrl = URL.createObjectURL(blob);
|
||||
return lastBlobUrl;
|
||||
lastSongBlobUrl = URL.createObjectURL(blob);
|
||||
return lastSongBlobUrl;
|
||||
} catch (error) {
|
||||
console.error("获取 Blob 链接遇到错误:" + error);
|
||||
throw error;
|
||||
|
||||
@@ -4,60 +4,113 @@
|
||||
* @param {string} data 接口数据
|
||||
* @returns {Array} 对应数据
|
||||
*/
|
||||
const parseLyric = (data) => {
|
||||
// 判断是否具有内容
|
||||
const checkLyric = (lyric) => (lyric ? (lyric.lyric ? true : false) : false);
|
||||
// 初始化数据
|
||||
const { lrc, tlyric, romalrc, yrc, ytlrc, yromalrc } = data;
|
||||
const lrcData = {
|
||||
lrc: lrc?.lyric || null,
|
||||
tlyric: tlyric?.lyric || null,
|
||||
romalrc: romalrc?.lyric || null,
|
||||
yrc: yrc?.lyric || null,
|
||||
ytlrc: ytlrc?.lyric || null,
|
||||
yromalrc: yromalrc?.lyric || null,
|
||||
};
|
||||
// 初始化输出结果
|
||||
const result = {
|
||||
// 是否具有普通翻译
|
||||
hasLrcTran: checkLyric(tlyric),
|
||||
// 是否具有普通音译
|
||||
hasLrcRoma: checkLyric(romalrc),
|
||||
// 是否具有逐字歌词
|
||||
hasYrc: checkLyric(yrc),
|
||||
// 是否具有逐字翻译
|
||||
hasYrcTran: checkLyric(ytlrc),
|
||||
// 是否具有逐字音译
|
||||
hasYrcRoma: checkLyric(yromalrc),
|
||||
// 普通歌词数组
|
||||
lrc: [],
|
||||
// 逐字歌词数据
|
||||
yrc: [],
|
||||
};
|
||||
// 普通歌词
|
||||
if (lrcData.lrc) {
|
||||
result.lrc = parseLrc(lrcData.lrc);
|
||||
//判断是否有其他翻译
|
||||
result.lrc = lrcData.tlyric
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.tlyric), "tran")
|
||||
: result.lrc;
|
||||
result.lrc = lrcData.romalrc
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.romalrc), "roma")
|
||||
: result.lrc;
|
||||
export const parseLyric = (data) => {
|
||||
try {
|
||||
// 判断是否具有内容
|
||||
const checkLyric = (lyric) => (lyric ? (lyric.lyric ? true : false) : false);
|
||||
// 初始化数据
|
||||
const { lrc, tlyric, romalrc, yrc, ytlrc, yromalrc } = data;
|
||||
const lrcData = {
|
||||
lrc: lrc?.lyric || null,
|
||||
tlyric: tlyric?.lyric || null,
|
||||
romalrc: romalrc?.lyric || null,
|
||||
yrc: yrc?.lyric || null,
|
||||
ytlrc: ytlrc?.lyric || null,
|
||||
yromalrc: yromalrc?.lyric || null,
|
||||
};
|
||||
// 初始化输出结果
|
||||
const result = {
|
||||
// 是否具有普通翻译
|
||||
hasLrcTran: checkLyric(tlyric),
|
||||
// 是否具有普通音译
|
||||
hasLrcRoma: checkLyric(romalrc),
|
||||
// 是否具有逐字歌词
|
||||
hasYrc: checkLyric(yrc),
|
||||
// 是否具有逐字翻译
|
||||
hasYrcTran: checkLyric(ytlrc),
|
||||
// 是否具有逐字音译
|
||||
hasYrcRoma: checkLyric(yromalrc),
|
||||
// 普通歌词数组
|
||||
lrc: [],
|
||||
// 逐字歌词数据
|
||||
yrc: [],
|
||||
};
|
||||
// 普通歌词
|
||||
if (lrcData.lrc) {
|
||||
result.lrc = parseLrc(lrcData.lrc);
|
||||
//判断是否有其他翻译
|
||||
result.lrc = lrcData.tlyric
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.tlyric), "tran")
|
||||
: result.lrc;
|
||||
result.lrc = lrcData.romalrc
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.romalrc), "roma")
|
||||
: result.lrc;
|
||||
}
|
||||
// 逐字歌词
|
||||
if (lrcData.yrc) {
|
||||
result.yrc = parseYrc(lrcData.yrc);
|
||||
//判断是否有其他翻译
|
||||
result.yrc = lrcData.ytlrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.ytlrc), "tran")
|
||||
: result.yrc;
|
||||
result.yrc = lrcData.yromalrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.yromalrc, false), "roma")
|
||||
: result.yrc;
|
||||
}
|
||||
console.log(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("解析歌词时出现错误:", error);
|
||||
return false;
|
||||
}
|
||||
// 逐字歌词
|
||||
if (lrcData.yrc) {
|
||||
result.yrc = parseYrc(lrcData.yrc);
|
||||
//判断是否有其他翻译
|
||||
result.yrc = lrcData.ytlrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.ytlrc), "tran")
|
||||
: result.yrc;
|
||||
result.yrc = lrcData.yromalrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.yromalrc, false), "roma")
|
||||
: result.yrc;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析本地歌词数据
|
||||
* @param {string} data - 歌词字符串
|
||||
* @returns {Object} - 包含解析后的歌词信息的对象
|
||||
*/
|
||||
export const parseLocalLrc = (data) => {
|
||||
try {
|
||||
const lyric = parseLrc(data);
|
||||
const parsedLyrics = [];
|
||||
// 初始化输出结果
|
||||
const result = {
|
||||
hasLrcTran: false,
|
||||
hasLrcRoma: false,
|
||||
hasYrc: false,
|
||||
hasYrcTran: false,
|
||||
hasYrcRoma: false,
|
||||
lrc: [],
|
||||
yrc: [],
|
||||
};
|
||||
// 遍历本地歌词数据
|
||||
for (let i = 0; i < lyric.length; i++) {
|
||||
// 当前歌词
|
||||
let currentObj = lyric[i];
|
||||
// 是否有相同时间
|
||||
let existingObj = parsedLyrics.find((v) => Number(v.time) === Number(currentObj.time));
|
||||
// 如果存在翻译
|
||||
if (existingObj) {
|
||||
result.hasLrcTran = true;
|
||||
existingObj.tran = currentObj.content;
|
||||
}
|
||||
// 若不存在翻译
|
||||
else {
|
||||
parsedLyrics.push({
|
||||
time: currentObj.time,
|
||||
content: currentObj.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 改变输出结果
|
||||
result.lrc = parsedLyrics;
|
||||
console.log(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("解析本地歌词时出现错误:", error);
|
||||
return false;
|
||||
}
|
||||
console.log(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -199,5 +252,3 @@ const parseYrc = (lyrics) => {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default parseLyric;
|
||||
|
||||
@@ -106,3 +106,31 @@ export const getCommentTime = (t) => {
|
||||
}月${userDate.getDate()}日 ${UH}:${Um}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台时间戳格式化
|
||||
* @param {number} timestamp - 要格式化的时间戳(毫秒)
|
||||
* @returns {string} - 格式化后的日期描述
|
||||
*/
|
||||
export const djFormatDate = (timestamp) => {
|
||||
const now = new Date();
|
||||
const targetDate = new Date(timestamp);
|
||||
const timeDiff = now - targetDate;
|
||||
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
|
||||
const daysDiff = Math.floor(timeDiff / oneDay);
|
||||
// 数字补零
|
||||
const formatNumber = (num) => {
|
||||
return num < 10 ? `0${num}` : num;
|
||||
};
|
||||
if (daysDiff === 0) {
|
||||
return "今日";
|
||||
} else if (daysDiff === 1) {
|
||||
return "昨日";
|
||||
} else if (daysDiff <= 7) {
|
||||
return `${daysDiff}天前`;
|
||||
} else if (targetDate.getFullYear() === now.getFullYear() - 1) {
|
||||
return `${targetDate.getFullYear()}-${formatNumber(targetDate.getMonth() + 1)}`;
|
||||
} else {
|
||||
return `${formatNumber(targetDate.getMonth() + 1)}-${formatNumber(targetDate.getDate())}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,13 +171,6 @@
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!searchValue" class="song-list">
|
||||
<SongList :data="djData" type="dj" />
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="djData?.length"
|
||||
:totalCount="totalCount"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
</div>
|
||||
<SongList v-else-if="searchData?.length" :data="searchData" type="dj" />
|
||||
<n-empty
|
||||
@@ -209,7 +202,7 @@
|
||||
import { NIcon } from "naive-ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { siteData } from "@/stores";
|
||||
import { getDjDetail, getDjProgram, likeDj } from "@/api/dj";
|
||||
import { fuzzySearch } from "@/utils/helper";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
@@ -221,20 +214,17 @@ import SvgIcon from "@/components/Global/SvgIcon";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
const { loadSize } = storeToRefs(settings);
|
||||
|
||||
// 电台数据
|
||||
const djId = ref(router.currentRoute.value.query.id);
|
||||
const pageNumber = ref(Number(router.currentRoute.value.query?.page) || 1);
|
||||
const djDetail = ref(null);
|
||||
const djData = ref(null);
|
||||
|
||||
// 模糊搜索数据
|
||||
const loadingMsg = ref(null);
|
||||
const searchValue = ref(null);
|
||||
const searchData = ref([]);
|
||||
const totalCount = ref(0);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
@@ -267,6 +257,8 @@ const getDjDetailData = async (id) => {
|
||||
const detail = await getDjDetail(id);
|
||||
// 基础信息
|
||||
djDetail.value = formatData(detail.data, "dj")[0];
|
||||
// 获取节目
|
||||
await getDjProgramData(djId.value, djDetail.value?.count);
|
||||
} catch (error) {
|
||||
console.error("获取电台信息出错:", error);
|
||||
$message.error("获取电台信息出现错误");
|
||||
@@ -274,16 +266,27 @@ const getDjDetailData = async (id) => {
|
||||
};
|
||||
|
||||
// 获取电台全部节目
|
||||
const getDjProgramData = async (id, limit = loadSize.value, offset = 0) => {
|
||||
const getDjProgramData = async (id, count) => {
|
||||
try {
|
||||
if (count === 0) return (djData.value = "empty");
|
||||
// 是否为超大歌单
|
||||
if (count >= 500) {
|
||||
loadingMsg.value = $message.loading("该电台节目数量过多,请稍等", {
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
// 循环获取
|
||||
let offset = 0;
|
||||
djData.value = [];
|
||||
const result = await getDjProgram(id, limit, offset);
|
||||
console.log(result);
|
||||
// 数据总数
|
||||
totalCount.value = result.count;
|
||||
if (totalCount.value === 0) return (djData.value = "empty");
|
||||
// 处理数据
|
||||
djData.value = formatData(result.programs, "dj");
|
||||
while (count === null || offset < count) {
|
||||
const result = await getDjProgram(id, 500, offset);
|
||||
const djDetail = formatData(result.programs, "dj");
|
||||
djData.value = djData.value.concat(djDetail);
|
||||
offset += 500;
|
||||
}
|
||||
// 关闭加载提示
|
||||
loadingMsg.value?.destroy();
|
||||
loadingMsg.value = null;
|
||||
} catch (error) {
|
||||
console.error("获取电台节目错误:", error);
|
||||
$message.error("获取电台节目出现错误");
|
||||
@@ -330,36 +333,8 @@ const likeOrDislike = debounce(async (id) => {
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 页数变化
|
||||
const pageNumberChange = (page) => {
|
||||
router.push({
|
||||
path: "/dj",
|
||||
query: { id: djId.value, page },
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
if (val.name === "dj") {
|
||||
// 更改参数
|
||||
pageNumber.value = Number(val.query?.page) || 1;
|
||||
djId.value = val.query?.id;
|
||||
// 调用接口
|
||||
await getDjDetailData(djId.value);
|
||||
await getDjProgramData(
|
||||
djId.value,
|
||||
loadSize.value,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getDjDetailData(djId.value);
|
||||
await getDjProgramData(djId.value, loadSize.value, (pageNumber.value - 1) * settings.loadSize);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
v-model:value="searchValue"
|
||||
:input-props="{ autoComplete: false }"
|
||||
class="search"
|
||||
placeholder="搜索"
|
||||
placeholder="模糊搜索"
|
||||
clearable
|
||||
@input="localSearch"
|
||||
>
|
||||
|
||||
@@ -516,6 +516,21 @@
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
同时下载歌曲元信息
|
||||
<n-text class="tip">为当前下载歌曲附加封面及歌词等元信息</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="downloadMeta" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">下载歌曲时同时下载封面</div>
|
||||
<n-switch v-model:value="downloadCover" :disabled="!downloadMeta" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">下载歌曲时同时下载歌词</div>
|
||||
<n-switch v-model:value="downloadLyrics" :disabled="!downloadMeta" :round="false" />
|
||||
</n-card>
|
||||
</div>
|
||||
<!-- 其他 -->
|
||||
<div class="set-type">
|
||||
@@ -611,6 +626,9 @@ const {
|
||||
showSpectrums,
|
||||
siderShowCover,
|
||||
useMusicCache,
|
||||
downloadMeta,
|
||||
downloadCover,
|
||||
downloadLyrics,
|
||||
} = storeToRefs(settings);
|
||||
|
||||
// 标签页数据
|
||||
|
||||
Reference in New Issue
Block a user