mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-30 03:47:22 +08:00
Merge pull request #123 from algerkong/feat/down-new
✨ feat: 歌曲下载内置封面歌词歌曲信息等,添加无限制下载功能,优化下载管理,支持清空下载记录
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"font-list": "^1.5.1",
|
"font-list": "^1.5.1",
|
||||||
"netease-cloud-music-api-alger": "^4.26.1",
|
"netease-cloud-music-api-alger": "^4.26.1",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
"vue-i18n": "9"
|
"vue-i18n": "9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
title: 'Download Manager',
|
title: 'Download Manager',
|
||||||
localMusic: 'Local Music',
|
localMusic: 'Local Music',
|
||||||
|
count: '{count} songs in total',
|
||||||
|
clearAll: 'Clear All',
|
||||||
tabs: {
|
tabs: {
|
||||||
downloading: 'Downloading',
|
downloading: 'Downloading',
|
||||||
downloaded: 'Downloaded'
|
downloaded: 'Downloaded'
|
||||||
@@ -27,7 +29,17 @@ export default {
|
|||||||
confirm: 'Delete',
|
confirm: 'Delete',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
success: 'Successfully deleted',
|
success: 'Successfully deleted',
|
||||||
failed: 'Failed to delete'
|
failed: 'Failed to delete',
|
||||||
|
fileNotFound: 'File not found or moved, removed from records',
|
||||||
|
recordRemoved: 'Failed to delete file, but removed from records'
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
title: 'Clear Download Records',
|
||||||
|
message:
|
||||||
|
'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.',
|
||||||
|
confirm: 'Clear',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
success: 'Download records cleared'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
downloadComplete: '{filename} download completed',
|
downloadComplete: '{filename} download completed',
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export default {
|
|||||||
shortcutDesc: 'Customize global shortcuts',
|
shortcutDesc: 'Customize global shortcuts',
|
||||||
download: 'Download Management',
|
download: 'Download Management',
|
||||||
downloadDesc: 'Always show download list button',
|
downloadDesc: 'Always show download list button',
|
||||||
|
unlimitedDownload: 'Unlimited Download',
|
||||||
|
unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs',
|
||||||
downloadPath: 'Download Directory',
|
downloadPath: 'Download Directory',
|
||||||
downloadPathDesc: 'Choose download location for music files'
|
downloadPathDesc: 'Choose download location for music files'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export default {
|
|||||||
downloadFailed: 'Download failed',
|
downloadFailed: 'Download failed',
|
||||||
downloadQueued: 'Added to download queue',
|
downloadQueued: 'Added to download queue',
|
||||||
addedToNextPlay: 'Added to play next',
|
addedToNextPlay: 'Added to play next',
|
||||||
getUrlFailed: 'Failed to get music download URL'
|
getUrlFailed: 'Failed to get music download URL, please check if logged in'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
title: '下载管理',
|
title: '下载管理',
|
||||||
localMusic: '本地音乐',
|
localMusic: '本地音乐',
|
||||||
|
count: '共 {count} 首歌曲',
|
||||||
|
clearAll: '清空记录',
|
||||||
tabs: {
|
tabs: {
|
||||||
downloading: '下载中',
|
downloading: '下载中',
|
||||||
downloaded: '已下载'
|
downloaded: '已下载'
|
||||||
@@ -27,7 +29,16 @@ export default {
|
|||||||
confirm: '确定删除',
|
confirm: '确定删除',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
success: '删除成功',
|
success: '删除成功',
|
||||||
failed: '删除失败'
|
failed: '删除失败',
|
||||||
|
fileNotFound: '文件不存在或已被移动,已从记录中移除',
|
||||||
|
recordRemoved: '文件删除失败,但已从记录中移除'
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
title: '清空下载记录',
|
||||||
|
message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。',
|
||||||
|
confirm: '确定清空',
|
||||||
|
cancel: '取消',
|
||||||
|
success: '下载记录已清空'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
downloadComplete: '{filename} 下载完成',
|
downloadComplete: '{filename} 下载完成',
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export default {
|
|||||||
shortcutDesc: '自定义全局快捷键',
|
shortcutDesc: '自定义全局快捷键',
|
||||||
download: '下载管理',
|
download: '下载管理',
|
||||||
downloadDesc: '是否始终显示下载列表按钮',
|
downloadDesc: '是否始终显示下载列表按钮',
|
||||||
|
unlimitedDownload: '无限制下载',
|
||||||
|
unlimitedDownloadDesc: '开启后将无限制下载音乐(可能出现下载失败的情况), 默认限制 300 首',
|
||||||
downloadPath: '下载目录',
|
downloadPath: '下载目录',
|
||||||
downloadPathDesc: '选择音乐文件的下载位置'
|
downloadPathDesc: '选择音乐文件的下载位置'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export default {
|
|||||||
downloadFailed: '下载失败',
|
downloadFailed: '下载失败',
|
||||||
downloadQueued: '已加入下载队列',
|
downloadQueued: '已加入下载队列',
|
||||||
addedToNextPlay: '已添加到下一首播放',
|
addedToNextPlay: '已添加到下一首播放',
|
||||||
getUrlFailed: '获取音乐下载地址失败'
|
getUrlFailed: '获取音乐下载地址失败,请检查是否登录'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+209
-10
@@ -1,9 +1,14 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { app, dialog, ipcMain, protocol, shell } from 'electron';
|
import { app, dialog, ipcMain, Notification, protocol, shell } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as NodeID3 from 'node-id3';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { getStore } from './config';
|
||||||
|
|
||||||
const MAX_CONCURRENT_DOWNLOADS = 3;
|
const MAX_CONCURRENT_DOWNLOADS = 3;
|
||||||
const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];
|
const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];
|
||||||
let activeDownloads = 0;
|
let activeDownloads = 0;
|
||||||
@@ -269,7 +274,7 @@ function sanitizeFilename(filename: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载音乐功能
|
* 下载音乐和歌词
|
||||||
*/
|
*/
|
||||||
async function downloadMusic(
|
async function downloadMusic(
|
||||||
event: Electron.IpcMainEvent,
|
event: Electron.IpcMainEvent,
|
||||||
@@ -284,8 +289,11 @@ async function downloadMusic(
|
|||||||
let writer: fs.WriteStream | null = null;
|
let writer: fs.WriteStream | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const store = new Store();
|
// 使用配置Store来获取设置
|
||||||
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
const configStore = getStore();
|
||||||
|
const downloadPath =
|
||||||
|
(configStore.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||||
|
const apiPort = configStore.get('set.musicApiPort') || 30488;
|
||||||
|
|
||||||
// 清理文件名中的非法字符
|
// 清理文件名中的非法字符
|
||||||
const sanitizedFilename = sanitizeFilename(filename);
|
const sanitizedFilename = sanitizeFilename(filename);
|
||||||
@@ -313,7 +321,9 @@ async function downloadMusic(
|
|||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
timeout: 30000 // 30秒超时
|
timeout: 30000, // 30秒超时
|
||||||
|
httpAgent: new http.Agent({ keepAlive: true }),
|
||||||
|
httpsAgent: new https.Agent({ keepAlive: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
writer = fs.createWriteStream(finalFilePath);
|
writer = fs.createWriteStream(finalFilePath);
|
||||||
@@ -351,9 +361,121 @@ async function downloadMusic(
|
|||||||
throw new Error('文件下载不完整');
|
throw new Error('文件下载不完整');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载歌词
|
||||||
|
let lyricData = null;
|
||||||
|
let lyricsContent = '';
|
||||||
|
try {
|
||||||
|
if (songInfo?.id) {
|
||||||
|
// 下载歌词,使用配置的端口
|
||||||
|
const lyricsResponse = await axios.get(
|
||||||
|
`http://localhost:${apiPort}/lyric?id=${songInfo.id}`
|
||||||
|
);
|
||||||
|
if (lyricsResponse.data && (lyricsResponse.data.lrc || lyricsResponse.data.tlyric)) {
|
||||||
|
lyricData = lyricsResponse.data;
|
||||||
|
|
||||||
|
// 处理歌词内容
|
||||||
|
if (lyricsResponse.data.lrc && lyricsResponse.data.lrc.lyric) {
|
||||||
|
lyricsContent = lyricsResponse.data.lrc.lyric;
|
||||||
|
|
||||||
|
// 如果有翻译歌词,合并到主歌词中
|
||||||
|
if (lyricsResponse.data.tlyric && lyricsResponse.data.tlyric.lyric) {
|
||||||
|
// 解析原歌词和翻译
|
||||||
|
const originalLyrics = parseLyrics(lyricsResponse.data.lrc.lyric);
|
||||||
|
const translatedLyrics = parseLyrics(lyricsResponse.data.tlyric.lyric);
|
||||||
|
|
||||||
|
// 合并歌词
|
||||||
|
const mergedLyrics = mergeLyrics(originalLyrics, translatedLyrics);
|
||||||
|
lyricsContent = mergedLyrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不再单独写入歌词文件,只保存在ID3标签中
|
||||||
|
console.log('歌词已准备好,将写入ID3标签');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (lyricError) {
|
||||||
|
console.error('下载歌词失败:', lyricError);
|
||||||
|
// 继续处理,不影响音乐下载
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载封面
|
||||||
|
let coverImageBuffer: Buffer | null = null;
|
||||||
|
try {
|
||||||
|
if (songInfo?.picUrl || songInfo?.al?.picUrl) {
|
||||||
|
const picUrl = songInfo.picUrl || songInfo.al?.picUrl;
|
||||||
|
if (picUrl && picUrl !== '/images/default_cover.png') {
|
||||||
|
const coverResponse = await axios({
|
||||||
|
url: picUrl.replace('http://', 'https://'),
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取封面图片的buffer
|
||||||
|
coverImageBuffer = Buffer.from(coverResponse.data);
|
||||||
|
|
||||||
|
// 不再单独保存封面文件,只保存在ID3标签中
|
||||||
|
console.log('封面已准备好,将写入ID3标签');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (coverError) {
|
||||||
|
console.error('下载封面失败:', coverError);
|
||||||
|
// 继续处理,不影响音乐下载
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在写入ID3标签前,先移除可能存在的旧标签
|
||||||
|
try {
|
||||||
|
NodeID3.removeTags(finalFilePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing existing ID3 tags:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强化ID3标签的写入格式
|
||||||
|
|
||||||
|
const artistNames =
|
||||||
|
(songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/ ') || '未知艺术家';
|
||||||
|
const tags = {
|
||||||
|
title: filename,
|
||||||
|
artist: artistNames,
|
||||||
|
TPE1: artistNames,
|
||||||
|
TPE2: artistNames,
|
||||||
|
album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename,
|
||||||
|
APIC: {
|
||||||
|
// 专辑封面
|
||||||
|
imageBuffer: coverImageBuffer,
|
||||||
|
type: {
|
||||||
|
id: 3,
|
||||||
|
name: 'front cover'
|
||||||
|
},
|
||||||
|
description: 'Album cover',
|
||||||
|
mime: 'image/jpeg'
|
||||||
|
},
|
||||||
|
USLT: {
|
||||||
|
// 歌词
|
||||||
|
language: 'chi',
|
||||||
|
description: 'Lyrics',
|
||||||
|
text: lyricsContent || ''
|
||||||
|
},
|
||||||
|
trackNumber: songInfo?.no || undefined,
|
||||||
|
year: songInfo?.publishTime
|
||||||
|
? new Date(songInfo.publishTime).getFullYear().toString()
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = NodeID3.write(tags, finalFilePath);
|
||||||
|
if (!success) {
|
||||||
|
console.error('Failed to write ID3 tags');
|
||||||
|
} else {
|
||||||
|
console.log('ID3 tags written successfully');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error writing ID3 tags:', err);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存下载信息
|
// 保存下载信息
|
||||||
try {
|
try {
|
||||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
const songInfos = configStore.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
const defaultInfo = {
|
const defaultInfo = {
|
||||||
name: filename,
|
name: filename,
|
||||||
ar: [{ name: '本地音乐' }],
|
ar: [{ name: '本地音乐' }],
|
||||||
@@ -364,24 +486,48 @@ async function downloadMusic(
|
|||||||
id: songInfo?.id || 0,
|
id: songInfo?.id || 0,
|
||||||
name: songInfo?.name || filename,
|
name: songInfo?.name || filename,
|
||||||
filename,
|
filename,
|
||||||
picUrl: songInfo?.picUrl || defaultInfo.picUrl,
|
picUrl: songInfo?.picUrl || songInfo?.al?.picUrl || defaultInfo.picUrl,
|
||||||
ar: songInfo?.ar || defaultInfo.ar,
|
ar: songInfo?.ar || defaultInfo.ar,
|
||||||
|
al: songInfo?.al || {
|
||||||
|
picUrl: songInfo?.picUrl || defaultInfo.picUrl,
|
||||||
|
name: songInfo?.name || filename
|
||||||
|
},
|
||||||
size: totalSize,
|
size: totalSize,
|
||||||
path: finalFilePath,
|
path: finalFilePath,
|
||||||
downloadTime: Date.now(),
|
downloadTime: Date.now(),
|
||||||
al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl },
|
type: type || 'mp3',
|
||||||
type: type || 'mp3'
|
lyric: lyricData
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存到下载记录
|
// 保存到下载记录
|
||||||
songInfos[finalFilePath] = newSongInfo;
|
songInfos[finalFilePath] = newSongInfo;
|
||||||
store.set('downloadedSongs', songInfos);
|
configStore.set('downloadedSongs', songInfos);
|
||||||
|
|
||||||
// 添加到下载历史
|
// 添加到下载历史
|
||||||
const history = downloadStore.get('history', []) as any[];
|
const history = downloadStore.get('history', []) as any[];
|
||||||
history.unshift(newSongInfo);
|
history.unshift(newSongInfo);
|
||||||
downloadStore.set('history', history);
|
downloadStore.set('history', history);
|
||||||
|
|
||||||
|
// 发送桌面通知
|
||||||
|
try {
|
||||||
|
const artistNames =
|
||||||
|
(songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/') ||
|
||||||
|
'未知艺术家';
|
||||||
|
const notification = new Notification({
|
||||||
|
title: '下载完成',
|
||||||
|
body: `${songInfo?.name || filename} - ${artistNames}`,
|
||||||
|
silent: false
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.on('click', () => {
|
||||||
|
shell.showItemInFolder(finalFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.show();
|
||||||
|
} catch (notifyError) {
|
||||||
|
console.error('发送通知失败:', notifyError);
|
||||||
|
}
|
||||||
|
|
||||||
// 发送下载完成事件
|
// 发送下载完成事件
|
||||||
event.reply('music-download-complete', {
|
event.reply('music-download-complete', {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -416,3 +562,56 @@ async function downloadMusic(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数 - 解析歌词文本成时间戳和内容的映射
|
||||||
|
function parseLyrics(lyricsText: string): Map<string, string> {
|
||||||
|
const lyricMap = new Map<string, string>();
|
||||||
|
const lines = lyricsText.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 匹配时间标签,形如 [00:00.000]
|
||||||
|
const timeTagMatches = line.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g);
|
||||||
|
if (!timeTagMatches) continue;
|
||||||
|
|
||||||
|
// 提取歌词内容(去除时间标签)
|
||||||
|
const content = line.replace(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g, '').trim();
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
// 将每个时间标签与歌词内容关联
|
||||||
|
for (const timeTag of timeTagMatches) {
|
||||||
|
lyricMap.set(timeTag, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lyricMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数 - 合并原文歌词和翻译歌词
|
||||||
|
function mergeLyrics(
|
||||||
|
originalLyrics: Map<string, string>,
|
||||||
|
translatedLyrics: Map<string, string>
|
||||||
|
): string {
|
||||||
|
const mergedLines: string[] = [];
|
||||||
|
|
||||||
|
// 对每个时间戳,组合原始歌词和翻译
|
||||||
|
for (const [timeTag, originalContent] of originalLyrics.entries()) {
|
||||||
|
const translatedContent = translatedLyrics.get(timeTag);
|
||||||
|
|
||||||
|
// 添加原始歌词行
|
||||||
|
mergedLines.push(`${timeTag}${originalContent}`);
|
||||||
|
|
||||||
|
// 如果有翻译,添加翻译行(时间戳相同,这样可以和原歌词同步显示)
|
||||||
|
if (translatedContent) {
|
||||||
|
mergedLines.push(`${timeTag}${translatedContent}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间顺序排序
|
||||||
|
mergedLines.sort((a, b) => {
|
||||||
|
const timeA = a.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || '';
|
||||||
|
const timeB = b.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || '';
|
||||||
|
return timeA.localeCompare(timeB);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedLines.join('\n');
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -20,5 +20,6 @@
|
|||||||
"autoPlay": false,
|
"autoPlay": false,
|
||||||
"downloadPath": "",
|
"downloadPath": "",
|
||||||
"language": "zh-CN",
|
"language": "zh-CN",
|
||||||
"alwaysShowDownloadButton": false
|
"alwaysShowDownloadButton": false,
|
||||||
|
"unlimitedDownload": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,17 @@
|
|||||||
<n-empty :description="t('download.empty.noDownloaded')" />
|
<n-empty :description="t('download.empty.noDownloaded')" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="downloaded-content">
|
<div v-else class="downloaded-content">
|
||||||
|
<div class="downloaded-header">
|
||||||
|
<div class="header-title">
|
||||||
|
{{ t('download.count', { count: downloadedList.length }) }}
|
||||||
|
</div>
|
||||||
|
<n-button secondary size="small" @click="showClearConfirm = true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-delete-bin-line mr-1"></i>
|
||||||
|
</template>
|
||||||
|
{{ t('download.clearAll') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
<div class="downloaded-items">
|
<div class="downloaded-items">
|
||||||
<div v-for="item in downList" :key="item.path" class="downloaded-item">
|
<div v-for="item in downList" :key="item.path" class="downloaded-item">
|
||||||
<div class="downloaded-item-content">
|
<div class="downloaded-item-content">
|
||||||
@@ -172,12 +183,38 @@
|
|||||||
}}</n-button>
|
}}</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 清空确认对话框 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showClearConfirm"
|
||||||
|
preset="dialog"
|
||||||
|
type="warning"
|
||||||
|
:title="t('download.clear.title')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="iconfont ri-delete-bin-line mr-2 text-xl"></i>
|
||||||
|
<span>{{ t('download.clear.title') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="delete-confirm-content">
|
||||||
|
{{ t('download.clear.message') }}
|
||||||
|
</div>
|
||||||
|
<template #action>
|
||||||
|
<n-button size="small" @click="showClearConfirm = false">{{
|
||||||
|
t('download.clear.cancel')
|
||||||
|
}}</n-button>
|
||||||
|
<n-button size="small" type="warning" @click="clearDownloadRecords">{{
|
||||||
|
t('download.clear.confirm')
|
||||||
|
}}</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ProgressStatus } from 'naive-ui';
|
import type { ProgressStatus } from 'naive-ui';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { getMusicDetail } from '@/api/music';
|
import { getMusicDetail } from '@/api/music';
|
||||||
@@ -320,29 +357,49 @@ const confirmDelete = async () => {
|
|||||||
'delete-downloaded-music',
|
'delete-downloaded-music',
|
||||||
itemToDelete.value.path
|
itemToDelete.value.path
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 无论删除文件是否成功,都从记录中移除
|
||||||
|
localStorage.setItem(
|
||||||
|
'downloadedList',
|
||||||
|
JSON.stringify(
|
||||||
|
downloadedList.value.filter((item) => item.id !== (itemToDelete.value as DownloadedItem).id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await refreshDownloadedList();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem(
|
|
||||||
'downloadedList',
|
|
||||||
JSON.stringify(
|
|
||||||
downloadedList.value.filter(
|
|
||||||
(item) => item.id !== (itemToDelete.value as DownloadedItem).id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await refreshDownloadedList();
|
|
||||||
message.success(t('download.delete.success'));
|
message.success(t('download.delete.success'));
|
||||||
} else {
|
} else {
|
||||||
message.error(t('download.delete.failed'));
|
message.warning(t('download.delete.fileNotFound'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete music:', error);
|
console.error('Failed to delete music:', error);
|
||||||
message.error(t('download.delete.failed'));
|
// 即使删除文件出错,也从记录中移除
|
||||||
|
localStorage.setItem(
|
||||||
|
'downloadedList',
|
||||||
|
JSON.stringify(
|
||||||
|
downloadedList.value.filter((item) => item.id !== (itemToDelete.value as DownloadedItem).id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await refreshDownloadedList();
|
||||||
|
message.warning(t('download.delete.recordRemoved'));
|
||||||
} finally {
|
} finally {
|
||||||
showDeleteConfirm.value = false;
|
showDeleteConfirm.value = false;
|
||||||
itemToDelete.value = null;
|
itemToDelete.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清空下载记录相关
|
||||||
|
const showClearConfirm = ref(false);
|
||||||
|
|
||||||
|
// 清空下载记录
|
||||||
|
const clearDownloadRecords = () => {
|
||||||
|
localStorage.setItem('downloadedList', '[]');
|
||||||
|
downloadedList.value = [];
|
||||||
|
message.success(t('download.clear.success'));
|
||||||
|
showClearConfirm.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 播放音乐
|
// 播放音乐
|
||||||
// const handlePlay = async (musicInfo: SongResult) => {
|
// const handlePlay = async (musicInfo: SongResult) => {
|
||||||
// await playerStore.setPlay(musicInfo);
|
// await playerStore.setPlay(musicInfo);
|
||||||
@@ -421,6 +478,12 @@ onMounted(() => {
|
|||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
window.electron.ipcRenderer.on('music-download-progress', (_, data) => {
|
window.electron.ipcRenderer.on('music-download-progress', (_, data) => {
|
||||||
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||||
|
|
||||||
|
// 如果进度为100%,将状态设置为已完成
|
||||||
|
if (data.progress === 100) {
|
||||||
|
data.status = 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
Object.assign(existingItem, {
|
Object.assign(existingItem, {
|
||||||
...data,
|
...data,
|
||||||
@@ -523,9 +586,18 @@ const handleDrawerClose = () => {
|
|||||||
@apply flex-1 overflow-hidden pb-40;
|
@apply flex-1 overflow-hidden pb-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloaded-header {
|
||||||
|
@apply flex items-center justify-between p-4 bg-light-100 dark:bg-dark-200 sticky top-0 z-10;
|
||||||
|
@apply border-b border-gray-100 dark:border-gray-800;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
@apply text-sm font-medium text-gray-600 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.download-items,
|
.download-items,
|
||||||
.downloaded-items {
|
.downloaded-items {
|
||||||
@apply space-y-3;
|
@apply space-y-3 p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-progress {
|
.total-progress {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ import { getSongUrl } from '@/hooks/MusicListHook';
|
|||||||
import { useArtist } from '@/hooks/useArtist';
|
import { useArtist } from '@/hooks/useArtist';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import { usePlayerStore } from '@/store';
|
import { usePlayerStore } from '@/store';
|
||||||
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isElectron } from '@/utils';
|
import { getImgUrl, isElectron } from '@/utils';
|
||||||
import { getImageBackground } from '@/utils/linearColor';
|
import { getImageBackground } from '@/utils/linearColor';
|
||||||
@@ -285,7 +286,14 @@ const downloadMusic = async () => {
|
|||||||
try {
|
try {
|
||||||
isDownloading.value = true;
|
isDownloading.value = true;
|
||||||
|
|
||||||
const data = (await getSongUrl(props.item.id as number, cloneDeep(props.item), true)) as any;
|
const settingsStore = useSettingsStore();
|
||||||
|
const { unlimitedDownload } = settingsStore.setData;
|
||||||
|
|
||||||
|
const data = (await getSongUrl(
|
||||||
|
props.item.id as number,
|
||||||
|
cloneDeep(props.item),
|
||||||
|
unlimitedDownload
|
||||||
|
)) as any;
|
||||||
if (!data || !data.url) {
|
if (!data || !data.url) {
|
||||||
throw new Error(t('songItem.message.getUrlFailed'));
|
throw new Error(t('songItem.message.getUrlFailed'));
|
||||||
}
|
}
|
||||||
@@ -293,14 +301,17 @@ const downloadMusic = async () => {
|
|||||||
// 构建文件名
|
// 构建文件名
|
||||||
const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(',');
|
const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(',');
|
||||||
const filename = `${props.item.name} - ${artistNames}`;
|
const filename = `${props.item.name} - ${artistNames}`;
|
||||||
|
console.log('props.item', props.item);
|
||||||
|
|
||||||
|
const songData = cloneDeep(props.item);
|
||||||
|
songData.ar = songData.ar || songData.song?.artists;
|
||||||
// 发送下载请求
|
// 发送下载请求
|
||||||
window.electron.ipcRenderer.send('download-music', {
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
url: data.url,
|
url: data.url,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
filename,
|
filename,
|
||||||
songInfo: {
|
songInfo: {
|
||||||
...cloneDeep(props.item),
|
...songData,
|
||||||
downloadTime: Date.now()
|
downloadTime: Date.now()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ref } from 'vue';
|
|||||||
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
|
import { useSettingsStore } from '@/store';
|
||||||
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
import { getImageLinearBackground } from '@/utils/linearColor';
|
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||||
@@ -13,12 +14,16 @@ const musicHistory = useMusicHistory();
|
|||||||
|
|
||||||
// 获取歌曲url
|
// 获取歌曲url
|
||||||
export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean = false) => {
|
export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean = false) => {
|
||||||
const { data } = await getMusicUrl(id, isDownloaded);
|
const settingsStore = useSettingsStore();
|
||||||
|
const { unlimitedDownload } = settingsStore.setData;
|
||||||
|
|
||||||
|
const { data } = await getMusicUrl(id, !unlimitedDownload);
|
||||||
let url = '';
|
let url = '';
|
||||||
let songDetail = null;
|
let songDetail = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
||||||
const res = await getParsingMusicUrl(id, songData);
|
const res = await getParsingMusicUrl(id, cloneDeep(songData));
|
||||||
url = res.data.data.url;
|
url = res.data.data.url;
|
||||||
songDetail = res.data.data;
|
songDetail = res.data.data;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -193,11 +193,18 @@ const handleBatchDownload = async () => {
|
|||||||
failCount++;
|
failCount++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const songData = cloneDeep(song);
|
||||||
|
const songInfo = {
|
||||||
|
...songData,
|
||||||
|
ar: songData.ar || songData.song?.artists,
|
||||||
|
downloadTime: Date.now()
|
||||||
|
};
|
||||||
|
console.log('songInfo', songInfo);
|
||||||
|
console.log('song', song);
|
||||||
window.electron.ipcRenderer.send('download-music', {
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
url,
|
url,
|
||||||
filename: `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`,
|
filename: `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`,
|
||||||
songInfo: cloneDeep(song),
|
songInfo,
|
||||||
type
|
type
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,6 +211,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">{{ t('settings.application.unlimitedDownload') }}</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
<n-switch v-model:value="setData.unlimitedDownload" class="mr-2">
|
||||||
|
<template #checked>{{ t('common.on') }}</template>
|
||||||
|
<template #unchecked>{{ t('common.off') }}</template>
|
||||||
|
</n-switch>
|
||||||
|
{{ t('settings.application.unlimitedDownloadDesc') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="set-item">
|
<div class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">{{ t('settings.application.downloadPath') }}</div>
|
<div class="set-item-title">{{ t('settings.application.downloadPath') }}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user