@@ -6,6 +6,11 @@ import * as http from 'http';
6
6
import * as https from 'https' ;
7
7
import * as NodeID3 from 'node-id3' ;
8
8
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' ;
9
14
10
15
import { getStore } from './config' ;
11
16
@@ -36,9 +41,18 @@ export function initializeFileManager() {
36
41
// 注册本地文件协议
37
42
protocol . registerFileProtocol ( 'local' , ( request , callback ) => {
38
43
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 - z A - Z ] : \/ / . test ( filePath ) ) {
50
+ filePath = filePath . slice ( 1 ) ;
51
+ }
52
+
53
+ // 还原为系统路径格式
54
+ filePath = path . normalize ( filePath ) ;
55
+
42
56
// 检查文件是否存在
43
57
if ( ! fs . existsSync ( filePath ) ) {
44
58
console . error ( 'File not found:' , filePath ) ;
@@ -53,6 +67,31 @@ export function initializeFileManager() {
53
67
}
54
68
} ) ;
55
69
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
+
56
95
// 通用的选择目录处理
57
96
ipcMain . handle ( 'select-directory' , async ( ) => {
58
97
const result = await dialog . showOpenDialog ( {
@@ -311,6 +350,7 @@ async function downloadMusic(
311
350
) {
312
351
let finalFilePath = '' ;
313
352
let writer : fs . WriteStream | null = null ;
353
+ let tempFilePath = '' ;
314
354
315
355
try {
316
356
// 使用配置Store来获取设置
@@ -322,25 +362,21 @@ async function downloadMusic(
322
362
// 清理文件名中的非法字符
323
363
const sanitizedFilename = sanitizeFilename ( filename ) ;
324
364
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 } ) ;
337
371
}
372
+
373
+ tempFilePath = path . join ( tempDir , `${ Date . now ( ) } _${ sanitizedFilename } .tmp` ) ;
338
374
339
375
// 先获取文件大小
340
376
const headResponse = await axios . head ( url ) ;
341
377
const totalSize = parseInt ( headResponse . headers [ 'content-length' ] || '0' , 10 ) ;
342
378
343
- // 开始下载
379
+ // 开始下载到临时文件
344
380
const response = await axios ( {
345
381
url,
346
382
method : 'GET' ,
@@ -350,7 +386,7 @@ async function downloadMusic(
350
386
httpsAgent : new https . Agent ( { keepAlive : true } )
351
387
} ) ;
352
388
353
- writer = fs . createWriteStream ( finalFilePath ) ;
389
+ writer = fs . createWriteStream ( tempFilePath ) ;
354
390
let downloadedSize = 0 ;
355
391
356
392
// 使用 data 事件来跟踪下载进度
@@ -362,7 +398,7 @@ async function downloadMusic(
362
398
progress,
363
399
loaded : downloadedSize ,
364
400
total : totalSize ,
365
- path : finalFilePath ,
401
+ path : tempFilePath ,
366
402
status : progress === 100 ? 'completed' : 'downloading' ,
367
403
songInfo : songInfo || {
368
404
name : filename ,
@@ -380,11 +416,77 @@ async function downloadMusic(
380
416
} ) ;
381
417
382
418
// 验证文件是否完整下载
383
- const stats = fs . statSync ( finalFilePath ) ;
419
+ const stats = fs . statSync ( tempFilePath ) ;
384
420
if ( stats . size !== totalSize ) {
385
421
throw new Error ( '文件下载不完整' ) ;
386
422
}
387
423
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
+
388
490
// 下载歌词
389
491
let lyricData = null ;
390
492
let lyricsContent = '' ;
@@ -413,8 +515,7 @@ async function downloadMusic(
413
515
}
414
516
}
415
517
416
- // 不再单独写入歌词文件,只保存在ID3标签中
417
- console . log ( '歌词已准备好,将写入ID3标签' ) ;
518
+ console . log ( '歌词已准备好,将写入元数据' ) ;
418
519
}
419
520
}
420
521
} catch ( lyricError ) {
@@ -437,64 +538,66 @@ async function downloadMusic(
437
538
438
539
// 获取封面图片的buffer
439
540
coverImageBuffer = Buffer . from ( coverResponse . data ) ;
440
-
441
- // 不再单独保存封面文件,只保存在ID3标签中
442
- console . log ( '封面已准备好,将写入ID3标签' ) ;
541
+ console . log ( '封面已准备好,将写入元数据' ) ;
443
542
}
444
543
}
445
544
} catch ( coverError ) {
446
545
console . error ( '下载封面失败:' , coverError ) ;
447
546
// 继续处理,不影响音乐下载
448
547
}
449
548
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 ( ) ;
459
550
const artistNames =
460
551
( 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
- } ;
488
552
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 ) ;
495
596
}
496
- } catch ( err ) {
497
- console . error ( 'Error writing ID3 tags:' , err ) ;
597
+ } else {
598
+ // 对于非MP3文件,使用music-metadata来写入元数据可能需要专门的库
599
+ // 或者根据不同文件类型使用专用工具,暂时只记录但不处理
600
+ console . log ( `文件类型 ${ fileFormat } 不支持使用NodeID3写入标签,跳过元数据写入` ) ;
498
601
}
499
602
500
603
// 保存下载信息
@@ -519,7 +622,7 @@ async function downloadMusic(
519
622
size : totalSize ,
520
623
path : finalFilePath ,
521
624
downloadTime : Date . now ( ) ,
522
- type : type || 'mp3' ,
625
+ type : fileExtension . substring ( 1 ) , // 去掉前面的点号,只保留扩展名
523
626
lyric : lyricData
524
627
} ;
525
628
@@ -571,6 +674,17 @@ async function downloadMusic(
571
674
if ( writer ) {
572
675
writer . end ( ) ;
573
676
}
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
+ // 清理未完成的最终文件
574
688
if ( finalFilePath && fs . existsSync ( finalFilePath ) ) {
575
689
try {
576
690
fs . unlinkSync ( finalFilePath ) ;
0 commit comments