Skip to content

Commit 3ac3159

Browse files
committed
feat: 添加下载管理页面, 引入文件类型检测库以支持多种音频格式
1 parent bfaa06b commit 3ac3159

File tree

7 files changed

+1217
-651
lines changed

7 files changed

+1217
-651
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
"electron-updater": "^6.6.2",
3131
"electron-window-state": "^5.0.3",
3232
"express": "^4.18.2",
33+
"file-type": "^21.0.0",
3334
"font-list": "^1.5.1",
3435
"husky": "^9.1.7",
36+
"music-metadata": "^11.2.3",
3537
"netease-cloud-music-api-alger": "^4.26.1",
3638
"node-id3": "^0.2.9",
3739
"node-machine-id": "^1.1.12",

src/i18n/lang/en-US/download.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,11 @@ export default {
4545
downloadComplete: '{filename} download completed',
4646
downloadFailed: '{filename} download failed: {error}'
4747
},
48-
loading: 'Loading...'
48+
loading: 'Loading...',
49+
playStarted: 'Play started: {name}',
50+
playFailed: 'Play failed: {name}',
51+
path: {
52+
copied: 'Path copied to clipboard',
53+
copyFailed: 'Failed to copy path'
54+
}
4955
};

src/i18n/lang/zh-CN/download.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,11 @@ export default {
4444
downloadComplete: '{filename} 下载完成',
4545
downloadFailed: '{filename} 下载失败: {error}'
4646
},
47-
loading: '加载中...'
47+
loading: '加载中...',
48+
playStarted: '开始播放: {name}',
49+
playFailed: '播放失败: {name}',
50+
path: {
51+
copied: '路径已复制到剪贴板',
52+
copyFailed: '复制路径失败'
53+
}
4854
};

src/main/modules/fileManager.ts

Lines changed: 183 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import * as http from 'http';
66
import * as https from 'https';
77
import * as NodeID3 from 'node-id3';
88
import * as path from 'path';
9+
import * as os from 'os';
10+
import * as mm from 'music-metadata';
11+
// 导入文件类型库,这里使用CommonJS兼容方式导入
12+
// 对于file-type v21.0.0需要这样导入
13+
import { fileTypeFromFile } from 'file-type';
914

1015
import { getStore } from './config';
1116

@@ -36,9 +41,18 @@ export function initializeFileManager() {
3641
// 注册本地文件协议
3742
protocol.registerFileProtocol('local', (request, callback) => {
3843
try {
39-
const decodedUrl = decodeURIComponent(request.url);
40-
const filePath = decodedUrl.replace('local://', '');
41-
44+
let url = request.url;
45+
// local://C:/Users/xxx.mp3
46+
let filePath = decodeURIComponent(url.replace('local:///', ''));
47+
48+
// 兼容 local:///C:/Users/xxx.mp3 这种情况
49+
if (/^\/[a-zA-Z]:\//.test(filePath)) {
50+
filePath = filePath.slice(1);
51+
}
52+
53+
// 还原为系统路径格式
54+
filePath = path.normalize(filePath);
55+
4256
// 检查文件是否存在
4357
if (!fs.existsSync(filePath)) {
4458
console.error('File not found:', filePath);
@@ -53,6 +67,31 @@ export function initializeFileManager() {
5367
}
5468
});
5569

70+
// 检查文件是否存在
71+
ipcMain.handle('check-file-exists', (_, filePath) => {
72+
try {
73+
return fs.existsSync(filePath);
74+
} catch (error) {
75+
console.error('Error checking if file exists:', error);
76+
return false;
77+
}
78+
});
79+
80+
// 获取支持的音频格式列表
81+
ipcMain.handle('get-supported-audio-formats', () => {
82+
return {
83+
formats: [
84+
{ ext: 'mp3', name: 'MP3' },
85+
{ ext: 'm4a', name: 'M4A/AAC' },
86+
{ ext: 'flac', name: 'FLAC' },
87+
{ ext: 'wav', name: 'WAV' },
88+
{ ext: 'ogg', name: 'OGG Vorbis' },
89+
{ ext: 'aac', name: 'AAC' }
90+
],
91+
default: 'mp3'
92+
};
93+
});
94+
5695
// 通用的选择目录处理
5796
ipcMain.handle('select-directory', async () => {
5897
const result = await dialog.showOpenDialog({
@@ -311,6 +350,7 @@ async function downloadMusic(
311350
) {
312351
let finalFilePath = '';
313352
let writer: fs.WriteStream | null = null;
353+
let tempFilePath = '';
314354

315355
try {
316356
// 使用配置Store来获取设置
@@ -322,25 +362,21 @@ async function downloadMusic(
322362
// 清理文件名中的非法字符
323363
const sanitizedFilename = sanitizeFilename(filename);
324364

325-
// 从URL中获取文件扩展名,如果没有则使用传入的type或默认mp3
326-
const urlExt = type ? `.${type}` : '.mp3';
327-
const filePath = path.join(downloadPath, `${sanitizedFilename}${urlExt}`);
328-
329-
// 检查文件是否已存在,如果存在则添加序号
330-
finalFilePath = filePath;
331-
let counter = 1;
332-
while (fs.existsSync(finalFilePath)) {
333-
const ext = path.extname(filePath);
334-
const nameWithoutExt = filePath.slice(0, -ext.length);
335-
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
336-
counter++;
365+
// 创建临时文件路径 (在系统临时目录中创建)
366+
const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp');
367+
368+
// 确保临时目录存在
369+
if (!fs.existsSync(tempDir)) {
370+
fs.mkdirSync(tempDir, { recursive: true });
337371
}
372+
373+
tempFilePath = path.join(tempDir, `${Date.now()}_${sanitizedFilename}.tmp`);
338374

339375
// 先获取文件大小
340376
const headResponse = await axios.head(url);
341377
const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10);
342378

343-
// 开始下载
379+
// 开始下载到临时文件
344380
const response = await axios({
345381
url,
346382
method: 'GET',
@@ -350,7 +386,7 @@ async function downloadMusic(
350386
httpsAgent: new https.Agent({ keepAlive: true })
351387
});
352388

353-
writer = fs.createWriteStream(finalFilePath);
389+
writer = fs.createWriteStream(tempFilePath);
354390
let downloadedSize = 0;
355391

356392
// 使用 data 事件来跟踪下载进度
@@ -362,7 +398,7 @@ async function downloadMusic(
362398
progress,
363399
loaded: downloadedSize,
364400
total: totalSize,
365-
path: finalFilePath,
401+
path: tempFilePath,
366402
status: progress === 100 ? 'completed' : 'downloading',
367403
songInfo: songInfo || {
368404
name: filename,
@@ -380,11 +416,77 @@ async function downloadMusic(
380416
});
381417

382418
// 验证文件是否完整下载
383-
const stats = fs.statSync(finalFilePath);
419+
const stats = fs.statSync(tempFilePath);
384420
if (stats.size !== totalSize) {
385421
throw new Error('文件下载不完整');
386422
}
387423

424+
// 检测文件类型
425+
let fileExtension = '';
426+
427+
try {
428+
// 首先尝试使用file-type库检测
429+
const fileType = await fileTypeFromFile(tempFilePath);
430+
if (fileType && fileType.ext) {
431+
fileExtension = `.${fileType.ext}`;
432+
console.log(`文件类型检测结果: ${fileType.mime}, 扩展名: ${fileExtension}`);
433+
} else {
434+
// 如果file-type无法识别,尝试使用music-metadata
435+
const metadata = await mm.parseFile(tempFilePath);
436+
if (metadata && metadata.format) {
437+
// 根据format.container或codec判断扩展名
438+
const formatInfo = metadata.format;
439+
const container = formatInfo.container || '';
440+
const codec = formatInfo.codec || '';
441+
442+
// 音频格式映射表
443+
const formatMap = {
444+
'mp3': ['MPEG', 'MP3', 'mp3'],
445+
'aac': ['AAC'],
446+
'flac': ['FLAC'],
447+
'ogg': ['Ogg', 'Vorbis'],
448+
'wav': ['WAV', 'PCM'],
449+
'm4a': ['M4A', 'MP4']
450+
};
451+
452+
// 查找匹配的格式
453+
const format = Object.entries(formatMap).find(([_, keywords]) =>
454+
keywords.some(keyword => container.includes(keyword) || codec.includes(keyword))
455+
);
456+
457+
// 设置文件扩展名,如果没找到则默认为mp3
458+
fileExtension = format ? `.${format[0]}` : '.mp3';
459+
460+
console.log(`music-metadata检测结果: 容器:${container}, 编码:${codec}, 扩展名: ${fileExtension}`);
461+
} else {
462+
// 两种方法都失败,使用传入的type或默认mp3
463+
fileExtension = type ? `.${type}` : '.mp3';
464+
console.log(`无法检测文件类型,使用默认扩展名: ${fileExtension}`);
465+
}
466+
}
467+
} catch (err) {
468+
console.error('检测文件类型失败:', err);
469+
// 检测失败,使用传入的type或默认mp3
470+
fileExtension = type ? `.${type}` : '.mp3';
471+
}
472+
473+
// 使用检测到的文件扩展名创建最终文件路径
474+
const filePath = path.join(downloadPath, `${sanitizedFilename}${fileExtension}`);
475+
476+
// 检查文件是否已存在,如果存在则添加序号
477+
finalFilePath = filePath;
478+
let counter = 1;
479+
while (fs.existsSync(finalFilePath)) {
480+
const ext = path.extname(filePath);
481+
const nameWithoutExt = filePath.slice(0, -ext.length);
482+
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
483+
counter++;
484+
}
485+
486+
// 将临时文件移动到最终位置
487+
fs.copyFileSync(tempFilePath, finalFilePath);
488+
fs.unlinkSync(tempFilePath); // 删除临时文件
489+
388490
// 下载歌词
389491
let lyricData = null;
390492
let lyricsContent = '';
@@ -413,8 +515,7 @@ async function downloadMusic(
413515
}
414516
}
415517

416-
// 不再单独写入歌词文件,只保存在ID3标签中
417-
console.log('歌词已准备好,将写入ID3标签');
518+
console.log('歌词已准备好,将写入元数据');
418519
}
419520
}
420521
} catch (lyricError) {
@@ -437,64 +538,66 @@ async function downloadMusic(
437538

438539
// 获取封面图片的buffer
439540
coverImageBuffer = Buffer.from(coverResponse.data);
440-
441-
// 不再单独保存封面文件,只保存在ID3标签中
442-
console.log('封面已准备好,将写入ID3标签');
541+
console.log('封面已准备好,将写入元数据');
443542
}
444543
}
445544
} catch (coverError) {
446545
console.error('下载封面失败:', coverError);
447546
// 继续处理,不影响音乐下载
448547
}
449548

450-
// 在写入ID3标签前,先移除可能存在的旧标签
451-
try {
452-
NodeID3.removeTags(finalFilePath);
453-
} catch (err) {
454-
console.error('Error removing existing ID3 tags:', err);
455-
}
456-
457-
// 强化ID3标签的写入格式
458-
549+
const fileFormat = fileExtension.toLowerCase();
459550
const artistNames =
460551
(songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/ ') || '未知艺术家';
461-
const tags = {
462-
title: filename,
463-
artist: artistNames,
464-
TPE1: artistNames,
465-
TPE2: artistNames,
466-
album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename,
467-
APIC: {
468-
// 专辑封面
469-
imageBuffer: coverImageBuffer,
470-
type: {
471-
id: 3,
472-
name: 'front cover'
473-
},
474-
description: 'Album cover',
475-
mime: 'image/jpeg'
476-
},
477-
USLT: {
478-
// 歌词
479-
language: 'chi',
480-
description: 'Lyrics',
481-
text: lyricsContent || ''
482-
},
483-
trackNumber: songInfo?.no || undefined,
484-
year: songInfo?.publishTime
485-
? new Date(songInfo.publishTime).getFullYear().toString()
486-
: undefined
487-
};
488552

489-
try {
490-
const success = NodeID3.write(tags, finalFilePath);
491-
if (!success) {
492-
console.error('Failed to write ID3 tags');
493-
} else {
494-
console.log('ID3 tags written successfully');
553+
// 根据文件类型处理元数据
554+
if (['.mp3'].includes(fileFormat)) {
555+
// 对MP3文件使用NodeID3处理ID3标签
556+
try {
557+
// 在写入ID3标签前,先移除可能存在的旧标签
558+
NodeID3.removeTags(finalFilePath);
559+
560+
const tags = {
561+
title: filename,
562+
artist: artistNames,
563+
TPE1: artistNames,
564+
TPE2: artistNames,
565+
album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename,
566+
APIC: {
567+
// 专辑封面
568+
imageBuffer: coverImageBuffer,
569+
type: {
570+
id: 3,
571+
name: 'front cover'
572+
},
573+
description: 'Album cover',
574+
mime: 'image/jpeg'
575+
},
576+
USLT: {
577+
// 歌词
578+
language: 'chi',
579+
description: 'Lyrics',
580+
text: lyricsContent || ''
581+
},
582+
trackNumber: songInfo?.no || undefined,
583+
year: songInfo?.publishTime
584+
? new Date(songInfo.publishTime).getFullYear().toString()
585+
: undefined
586+
};
587+
588+
const success = NodeID3.write(tags, finalFilePath);
589+
if (!success) {
590+
console.error('Failed to write ID3 tags');
591+
} else {
592+
console.log('ID3 tags written successfully');
593+
}
594+
} catch (err) {
595+
console.error('Error writing ID3 tags:', err);
495596
}
496-
} catch (err) {
497-
console.error('Error writing ID3 tags:', err);
597+
} else {
598+
// 对于非MP3文件,使用music-metadata来写入元数据可能需要专门的库
599+
// 或者根据不同文件类型使用专用工具,暂时只记录但不处理
600+
console.log(`文件类型 ${fileFormat} 不支持使用NodeID3写入标签,跳过元数据写入`);
498601
}
499602

500603
// 保存下载信息
@@ -519,7 +622,7 @@ async function downloadMusic(
519622
size: totalSize,
520623
path: finalFilePath,
521624
downloadTime: Date.now(),
522-
type: type || 'mp3',
625+
type: fileExtension.substring(1), // 去掉前面的点号,只保留扩展名
523626
lyric: lyricData
524627
};
525628

@@ -571,6 +674,17 @@ async function downloadMusic(
571674
if (writer) {
572675
writer.end();
573676
}
677+
678+
// 清理临时文件
679+
if (tempFilePath && fs.existsSync(tempFilePath)) {
680+
try {
681+
fs.unlinkSync(tempFilePath);
682+
} catch (e) {
683+
console.error('Failed to delete temporary file:', e);
684+
}
685+
}
686+
687+
// 清理未完成的最终文件
574688
if (finalFilePath && fs.existsSync(finalFilePath)) {
575689
try {
576690
fs.unlinkSync(finalFilePath);

0 commit comments

Comments
 (0)