@huyooo/file-explorer-core 0.3.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1380 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -12
- package/dist/index.d.ts +427 -12
- package/dist/index.js +1381 -127
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -25,22 +25,22 @@ function encodeFileUrl(filePath) {
|
|
|
25
25
|
}
|
|
26
26
|
function decodeFileUrl(url) {
|
|
27
27
|
if (!url) return "";
|
|
28
|
-
let
|
|
29
|
-
if (
|
|
30
|
-
|
|
28
|
+
let path18 = url;
|
|
29
|
+
if (path18.startsWith(APP_PROTOCOL_PREFIX)) {
|
|
30
|
+
path18 = path18.slice(APP_PROTOCOL_PREFIX.length);
|
|
31
31
|
}
|
|
32
|
-
const hashIndex =
|
|
32
|
+
const hashIndex = path18.indexOf("#");
|
|
33
33
|
if (hashIndex !== -1) {
|
|
34
|
-
|
|
34
|
+
path18 = path18.substring(0, hashIndex);
|
|
35
35
|
}
|
|
36
|
-
const queryIndex =
|
|
36
|
+
const queryIndex = path18.indexOf("?");
|
|
37
37
|
if (queryIndex !== -1) {
|
|
38
|
-
|
|
38
|
+
path18 = path18.substring(0, queryIndex);
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
|
-
return decodeURIComponent(
|
|
41
|
+
return decodeURIComponent(path18);
|
|
42
42
|
} catch {
|
|
43
|
-
return
|
|
43
|
+
return path18;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function isAppProtocolUrl(url) {
|
|
@@ -388,16 +388,16 @@ async function createFile(filePath, content = "") {
|
|
|
388
388
|
await fs2.writeFile(filePath, content, "utf-8");
|
|
389
389
|
return { success: true, data: { finalPath: filePath } };
|
|
390
390
|
}
|
|
391
|
-
const
|
|
391
|
+
const dirname3 = path3.dirname(filePath);
|
|
392
392
|
const ext = path3.extname(filePath);
|
|
393
|
-
const
|
|
393
|
+
const basename2 = path3.basename(filePath, ext);
|
|
394
394
|
let counter = 2;
|
|
395
|
-
let finalPath = path3.join(
|
|
395
|
+
let finalPath = path3.join(dirname3, `${basename2} ${counter}${ext}`);
|
|
396
396
|
while (true) {
|
|
397
397
|
try {
|
|
398
398
|
await fs2.access(finalPath);
|
|
399
399
|
counter++;
|
|
400
|
-
finalPath = path3.join(
|
|
400
|
+
finalPath = path3.join(dirname3, `${basename2} ${counter}${ext}`);
|
|
401
401
|
} catch {
|
|
402
402
|
break;
|
|
403
403
|
}
|
|
@@ -573,25 +573,487 @@ async function isDirectory(filePath) {
|
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// src/operations/shell.ts
|
|
577
|
+
import { exec } from "child_process";
|
|
578
|
+
import { promisify } from "util";
|
|
579
|
+
import * as path6 from "path";
|
|
580
|
+
var execAsync = promisify(exec);
|
|
581
|
+
function getPlatform() {
|
|
582
|
+
switch (process.platform) {
|
|
583
|
+
case "darwin":
|
|
584
|
+
return "mac";
|
|
585
|
+
case "win32":
|
|
586
|
+
return "windows";
|
|
587
|
+
default:
|
|
588
|
+
return "linux";
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function showFileInfo(filePath) {
|
|
592
|
+
const platform2 = getPlatform();
|
|
593
|
+
try {
|
|
594
|
+
switch (platform2) {
|
|
595
|
+
case "mac": {
|
|
596
|
+
const script = `tell application "Finder"
|
|
597
|
+
activate
|
|
598
|
+
set theFile to POSIX file "${filePath}" as alias
|
|
599
|
+
open information window of theFile
|
|
600
|
+
end tell`;
|
|
601
|
+
await execAsync(`osascript -e '${script}'`);
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case "windows": {
|
|
605
|
+
const escapedPath = filePath.replace(/'/g, "''");
|
|
606
|
+
const psScript = `
|
|
607
|
+
$shell = New-Object -ComObject Shell.Application
|
|
608
|
+
$folder = $shell.Namespace((Split-Path '${escapedPath}'))
|
|
609
|
+
$item = $folder.ParseName((Split-Path '${escapedPath}' -Leaf))
|
|
610
|
+
$item.InvokeVerb('properties')
|
|
611
|
+
`;
|
|
612
|
+
await execAsync(`powershell -Command "${psScript.replace(/\n/g, " ")}"`);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case "linux": {
|
|
616
|
+
const commands = [
|
|
617
|
+
`nautilus --select "${filePath}" && nautilus -q`,
|
|
618
|
+
// GNOME
|
|
619
|
+
`dolphin --select "${filePath}"`,
|
|
620
|
+
// KDE
|
|
621
|
+
`thunar --quit && thunar "${path6.dirname(filePath)}"`,
|
|
622
|
+
// XFCE
|
|
623
|
+
`xdg-open "${path6.dirname(filePath)}"`
|
|
624
|
+
// 通用
|
|
625
|
+
];
|
|
626
|
+
let lastError = null;
|
|
627
|
+
for (const cmd of commands) {
|
|
628
|
+
try {
|
|
629
|
+
await execAsync(cmd);
|
|
630
|
+
return { success: true };
|
|
631
|
+
} catch (e) {
|
|
632
|
+
lastError = e;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
throw lastError || new Error("No file manager found");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { success: true };
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return {
|
|
641
|
+
success: false,
|
|
642
|
+
error: error instanceof Error ? error.message : String(error)
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function revealInFileManager(filePath) {
|
|
647
|
+
const platform2 = getPlatform();
|
|
648
|
+
try {
|
|
649
|
+
switch (platform2) {
|
|
650
|
+
case "mac": {
|
|
651
|
+
await execAsync(`open -R "${filePath}"`);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case "windows": {
|
|
655
|
+
await execAsync(`explorer.exe /select,"${filePath}"`);
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "linux": {
|
|
659
|
+
await execAsync(`xdg-open "${path6.dirname(filePath)}"`);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { success: true };
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return {
|
|
666
|
+
success: false,
|
|
667
|
+
error: error instanceof Error ? error.message : String(error)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function openInTerminal(dirPath) {
|
|
672
|
+
const platform2 = getPlatform();
|
|
673
|
+
try {
|
|
674
|
+
switch (platform2) {
|
|
675
|
+
case "mac": {
|
|
676
|
+
try {
|
|
677
|
+
const itermScript = `tell application "iTerm"
|
|
678
|
+
activate
|
|
679
|
+
try
|
|
680
|
+
set newWindow to (create window with default profile)
|
|
681
|
+
tell current session of newWindow
|
|
682
|
+
write text "cd '${dirPath}'"
|
|
683
|
+
end tell
|
|
684
|
+
on error
|
|
685
|
+
tell current window
|
|
686
|
+
create tab with default profile
|
|
687
|
+
tell current session
|
|
688
|
+
write text "cd '${dirPath}'"
|
|
689
|
+
end tell
|
|
690
|
+
end tell
|
|
691
|
+
end try
|
|
692
|
+
end tell`;
|
|
693
|
+
await execAsync(`osascript -e '${itermScript}'`);
|
|
694
|
+
} catch {
|
|
695
|
+
const terminalScript = `tell application "Terminal"
|
|
696
|
+
activate
|
|
697
|
+
do script "cd '${dirPath}'"
|
|
698
|
+
end tell`;
|
|
699
|
+
await execAsync(`osascript -e '${terminalScript}'`);
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
case "windows": {
|
|
704
|
+
try {
|
|
705
|
+
await execAsync(`wt -d "${dirPath}"`);
|
|
706
|
+
} catch {
|
|
707
|
+
await execAsync(`start cmd /K "cd /d ${dirPath}"`);
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
case "linux": {
|
|
712
|
+
const terminals = [
|
|
713
|
+
`gnome-terminal --working-directory="${dirPath}"`,
|
|
714
|
+
`konsole --workdir "${dirPath}"`,
|
|
715
|
+
`xfce4-terminal --working-directory="${dirPath}"`,
|
|
716
|
+
`xterm -e "cd '${dirPath}' && $SHELL"`
|
|
717
|
+
];
|
|
718
|
+
let lastError = null;
|
|
719
|
+
for (const cmd of terminals) {
|
|
720
|
+
try {
|
|
721
|
+
await execAsync(cmd);
|
|
722
|
+
return { success: true };
|
|
723
|
+
} catch (e) {
|
|
724
|
+
lastError = e;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
throw lastError || new Error("No terminal found");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return { success: true };
|
|
731
|
+
} catch (error) {
|
|
732
|
+
return {
|
|
733
|
+
success: false,
|
|
734
|
+
error: error instanceof Error ? error.message : String(error)
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function openInEditor(targetPath) {
|
|
739
|
+
const platform2 = getPlatform();
|
|
740
|
+
try {
|
|
741
|
+
const editors = platform2 === "mac" ? [
|
|
742
|
+
// macOS: 优先 Cursor,回退到 VSCode
|
|
743
|
+
`open -a "Cursor" "${targetPath}"`,
|
|
744
|
+
`open -a "Visual Studio Code" "${targetPath}"`,
|
|
745
|
+
`code "${targetPath}"`
|
|
746
|
+
] : platform2 === "windows" ? [
|
|
747
|
+
// Windows
|
|
748
|
+
`cursor "${targetPath}"`,
|
|
749
|
+
`code "${targetPath}"`
|
|
750
|
+
] : [
|
|
751
|
+
// Linux
|
|
752
|
+
`cursor "${targetPath}"`,
|
|
753
|
+
`code "${targetPath}"`
|
|
754
|
+
];
|
|
755
|
+
let lastError = null;
|
|
756
|
+
for (const cmd of editors) {
|
|
757
|
+
try {
|
|
758
|
+
await execAsync(cmd);
|
|
759
|
+
return { success: true };
|
|
760
|
+
} catch (e) {
|
|
761
|
+
lastError = e;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
throw lastError || new Error("No editor found");
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return {
|
|
767
|
+
success: false,
|
|
768
|
+
error: error instanceof Error ? error.message : String(error)
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/operations/compress.ts
|
|
774
|
+
import * as compressing from "compressing";
|
|
775
|
+
import * as path7 from "path";
|
|
776
|
+
import * as fs7 from "fs";
|
|
777
|
+
import { promises as fsPromises } from "fs";
|
|
778
|
+
import { pipeline } from "stream/promises";
|
|
779
|
+
function getExtension(format) {
|
|
780
|
+
switch (format) {
|
|
781
|
+
case "zip":
|
|
782
|
+
return ".zip";
|
|
783
|
+
case "tar":
|
|
784
|
+
return ".tar";
|
|
785
|
+
case "tgz":
|
|
786
|
+
return ".tar.gz";
|
|
787
|
+
case "tarbz2":
|
|
788
|
+
return ".tar.bz2";
|
|
789
|
+
default:
|
|
790
|
+
return ".zip";
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function detectArchiveFormat(filePath) {
|
|
794
|
+
const lowerPath = filePath.toLowerCase();
|
|
795
|
+
if (lowerPath.endsWith(".zip")) return "zip";
|
|
796
|
+
if (lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz")) return "tgz";
|
|
797
|
+
if (lowerPath.endsWith(".tar.bz2") || lowerPath.endsWith(".tbz2")) return "tarbz2";
|
|
798
|
+
if (lowerPath.endsWith(".tar")) return "tar";
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
function isArchiveFile(filePath) {
|
|
802
|
+
return detectArchiveFormat(filePath) !== null;
|
|
803
|
+
}
|
|
804
|
+
async function getAllFiles(dirPath) {
|
|
805
|
+
const files = [];
|
|
806
|
+
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
807
|
+
for (const entry of entries) {
|
|
808
|
+
const fullPath = path7.join(dirPath, entry.name);
|
|
809
|
+
if (entry.isDirectory()) {
|
|
810
|
+
files.push(...await getAllFiles(fullPath));
|
|
811
|
+
} else {
|
|
812
|
+
files.push(fullPath);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return files;
|
|
816
|
+
}
|
|
817
|
+
async function countFiles(sources) {
|
|
818
|
+
let count = 0;
|
|
819
|
+
for (const source of sources) {
|
|
820
|
+
const stats = await fsPromises.stat(source);
|
|
821
|
+
if (stats.isDirectory()) {
|
|
822
|
+
const files = await getAllFiles(source);
|
|
823
|
+
count += files.length;
|
|
824
|
+
} else {
|
|
825
|
+
count += 1;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return count;
|
|
829
|
+
}
|
|
830
|
+
async function compressFiles(sources, options, onProgress) {
|
|
831
|
+
try {
|
|
832
|
+
const { format, level = "normal", outputName, outputDir, deleteSource } = options;
|
|
833
|
+
const ext = getExtension(format);
|
|
834
|
+
const finalName = outputName.endsWith(ext) ? outputName : outputName + ext;
|
|
835
|
+
const outputPath = path7.join(outputDir, finalName);
|
|
836
|
+
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
837
|
+
const totalCount = await countFiles(sources);
|
|
838
|
+
let processedCount = 0;
|
|
839
|
+
switch (format) {
|
|
840
|
+
case "zip": {
|
|
841
|
+
const stream = new compressing.zip.Stream();
|
|
842
|
+
for (const source of sources) {
|
|
843
|
+
const stats = await fsPromises.stat(source);
|
|
844
|
+
const baseName = path7.basename(source);
|
|
845
|
+
if (stats.isDirectory()) {
|
|
846
|
+
const files = await getAllFiles(source);
|
|
847
|
+
for (const file of files) {
|
|
848
|
+
const relativePath = path7.relative(path7.dirname(source), file);
|
|
849
|
+
stream.addEntry(file, { relativePath });
|
|
850
|
+
processedCount++;
|
|
851
|
+
onProgress?.({
|
|
852
|
+
currentFile: path7.basename(file),
|
|
853
|
+
processedCount,
|
|
854
|
+
totalCount,
|
|
855
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
stream.addEntry(source, { relativePath: baseName });
|
|
860
|
+
processedCount++;
|
|
861
|
+
onProgress?.({
|
|
862
|
+
currentFile: baseName,
|
|
863
|
+
processedCount,
|
|
864
|
+
totalCount,
|
|
865
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const destStream = fs7.createWriteStream(outputPath);
|
|
870
|
+
await pipeline(stream, destStream);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "tar": {
|
|
874
|
+
const stream = new compressing.tar.Stream();
|
|
875
|
+
for (const source of sources) {
|
|
876
|
+
const stats = await fsPromises.stat(source);
|
|
877
|
+
const baseName = path7.basename(source);
|
|
878
|
+
if (stats.isDirectory()) {
|
|
879
|
+
const files = await getAllFiles(source);
|
|
880
|
+
for (const file of files) {
|
|
881
|
+
const relativePath = path7.relative(path7.dirname(source), file);
|
|
882
|
+
stream.addEntry(file, { relativePath });
|
|
883
|
+
processedCount++;
|
|
884
|
+
onProgress?.({
|
|
885
|
+
currentFile: path7.basename(file),
|
|
886
|
+
processedCount,
|
|
887
|
+
totalCount,
|
|
888
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
stream.addEntry(source, { relativePath: baseName });
|
|
893
|
+
processedCount++;
|
|
894
|
+
onProgress?.({
|
|
895
|
+
currentFile: baseName,
|
|
896
|
+
processedCount,
|
|
897
|
+
totalCount,
|
|
898
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const destStream = fs7.createWriteStream(outputPath);
|
|
903
|
+
await pipeline(stream, destStream);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "tgz": {
|
|
907
|
+
const stream = new compressing.tgz.Stream();
|
|
908
|
+
for (const source of sources) {
|
|
909
|
+
const stats = await fsPromises.stat(source);
|
|
910
|
+
const baseName = path7.basename(source);
|
|
911
|
+
if (stats.isDirectory()) {
|
|
912
|
+
const files = await getAllFiles(source);
|
|
913
|
+
for (const file of files) {
|
|
914
|
+
const relativePath = path7.relative(path7.dirname(source), file);
|
|
915
|
+
stream.addEntry(file, { relativePath });
|
|
916
|
+
processedCount++;
|
|
917
|
+
onProgress?.({
|
|
918
|
+
currentFile: path7.basename(file),
|
|
919
|
+
processedCount,
|
|
920
|
+
totalCount,
|
|
921
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
stream.addEntry(source, { relativePath: baseName });
|
|
926
|
+
processedCount++;
|
|
927
|
+
onProgress?.({
|
|
928
|
+
currentFile: baseName,
|
|
929
|
+
processedCount,
|
|
930
|
+
totalCount,
|
|
931
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const destStream = fs7.createWriteStream(outputPath);
|
|
936
|
+
await pipeline(stream, destStream);
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
case "tarbz2": {
|
|
940
|
+
console.warn("tar.bz2 format not fully supported, using tgz instead");
|
|
941
|
+
const tgzStream = new compressing.tgz.Stream();
|
|
942
|
+
for (const source of sources) {
|
|
943
|
+
const stats = await fsPromises.stat(source);
|
|
944
|
+
const baseName = path7.basename(source);
|
|
945
|
+
if (stats.isDirectory()) {
|
|
946
|
+
const files = await getAllFiles(source);
|
|
947
|
+
for (const file of files) {
|
|
948
|
+
const relativePath = path7.relative(path7.dirname(source), file);
|
|
949
|
+
tgzStream.addEntry(file, { relativePath });
|
|
950
|
+
processedCount++;
|
|
951
|
+
onProgress?.({
|
|
952
|
+
currentFile: path7.basename(file),
|
|
953
|
+
processedCount,
|
|
954
|
+
totalCount,
|
|
955
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
tgzStream.addEntry(source, { relativePath: baseName });
|
|
960
|
+
processedCount++;
|
|
961
|
+
onProgress?.({
|
|
962
|
+
currentFile: baseName,
|
|
963
|
+
processedCount,
|
|
964
|
+
totalCount,
|
|
965
|
+
percent: Math.round(processedCount / totalCount * 100)
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const destStream = fs7.createWriteStream(outputPath.replace(".tar.bz2", ".tar.gz"));
|
|
970
|
+
await pipeline(tgzStream, destStream);
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (deleteSource) {
|
|
975
|
+
for (const source of sources) {
|
|
976
|
+
const stats = await fsPromises.stat(source);
|
|
977
|
+
if (stats.isDirectory()) {
|
|
978
|
+
await fsPromises.rm(source, { recursive: true });
|
|
979
|
+
} else {
|
|
980
|
+
await fsPromises.unlink(source);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { success: true, outputPath };
|
|
985
|
+
} catch (error) {
|
|
986
|
+
return {
|
|
987
|
+
success: false,
|
|
988
|
+
error: error instanceof Error ? error.message : String(error)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async function extractArchive(archivePath, options, onProgress) {
|
|
993
|
+
try {
|
|
994
|
+
const { targetDir, deleteArchive } = options;
|
|
995
|
+
const format = detectArchiveFormat(archivePath);
|
|
996
|
+
if (!format) {
|
|
997
|
+
return { success: false, error: "\u4E0D\u652F\u6301\u7684\u538B\u7F29\u683C\u5F0F" };
|
|
998
|
+
}
|
|
999
|
+
await fsPromises.mkdir(targetDir, { recursive: true });
|
|
1000
|
+
onProgress?.({
|
|
1001
|
+
currentFile: path7.basename(archivePath),
|
|
1002
|
+
processedCount: 0,
|
|
1003
|
+
totalCount: 1,
|
|
1004
|
+
percent: 0
|
|
1005
|
+
});
|
|
1006
|
+
switch (format) {
|
|
1007
|
+
case "zip":
|
|
1008
|
+
await compressing.zip.uncompress(archivePath, targetDir);
|
|
1009
|
+
break;
|
|
1010
|
+
case "tar":
|
|
1011
|
+
await compressing.tar.uncompress(archivePath, targetDir);
|
|
1012
|
+
break;
|
|
1013
|
+
case "tgz":
|
|
1014
|
+
await compressing.tgz.uncompress(archivePath, targetDir);
|
|
1015
|
+
break;
|
|
1016
|
+
case "tarbz2":
|
|
1017
|
+
console.warn("tar.bz2 format not fully supported");
|
|
1018
|
+
return { success: false, error: "tar.bz2 \u683C\u5F0F\u6682\u4E0D\u652F\u6301" };
|
|
1019
|
+
}
|
|
1020
|
+
onProgress?.({
|
|
1021
|
+
currentFile: path7.basename(archivePath),
|
|
1022
|
+
processedCount: 1,
|
|
1023
|
+
totalCount: 1,
|
|
1024
|
+
percent: 100
|
|
1025
|
+
});
|
|
1026
|
+
if (deleteArchive) {
|
|
1027
|
+
await fsPromises.unlink(archivePath);
|
|
1028
|
+
}
|
|
1029
|
+
return { success: true, outputPath: targetDir };
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
return {
|
|
1032
|
+
success: false,
|
|
1033
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
576
1038
|
// src/system-paths.ts
|
|
577
|
-
import
|
|
1039
|
+
import path8 from "path";
|
|
578
1040
|
import os from "os";
|
|
579
1041
|
var platform = process.platform;
|
|
580
1042
|
function getSystemPath(pathId) {
|
|
581
1043
|
const homeDir = os.homedir();
|
|
582
1044
|
switch (pathId) {
|
|
583
1045
|
case "desktop":
|
|
584
|
-
return
|
|
1046
|
+
return path8.join(homeDir, "Desktop");
|
|
585
1047
|
case "documents":
|
|
586
|
-
return
|
|
1048
|
+
return path8.join(homeDir, "Documents");
|
|
587
1049
|
case "downloads":
|
|
588
|
-
return
|
|
1050
|
+
return path8.join(homeDir, "Downloads");
|
|
589
1051
|
case "pictures":
|
|
590
|
-
return
|
|
1052
|
+
return path8.join(homeDir, "Pictures");
|
|
591
1053
|
case "music":
|
|
592
|
-
return
|
|
1054
|
+
return path8.join(homeDir, "Music");
|
|
593
1055
|
case "videos":
|
|
594
|
-
return platform === "darwin" ?
|
|
1056
|
+
return platform === "darwin" ? path8.join(homeDir, "Movies") : path8.join(homeDir, "Videos");
|
|
595
1057
|
case "applications":
|
|
596
1058
|
return platform === "darwin" ? "/Applications" : platform === "win32" ? process.env.ProgramFiles || "C:\\Program Files" : "/usr/share/applications";
|
|
597
1059
|
case "home":
|
|
@@ -623,14 +1085,14 @@ function getAllSystemPaths() {
|
|
|
623
1085
|
function getHomeDirectory() {
|
|
624
1086
|
return os.homedir();
|
|
625
1087
|
}
|
|
626
|
-
function
|
|
1088
|
+
function getPlatform2() {
|
|
627
1089
|
return process.platform;
|
|
628
1090
|
}
|
|
629
1091
|
|
|
630
1092
|
// src/search.ts
|
|
631
1093
|
import { fdir } from "fdir";
|
|
632
|
-
import
|
|
633
|
-
import { promises as
|
|
1094
|
+
import path9 from "path";
|
|
1095
|
+
import { promises as fs8 } from "fs";
|
|
634
1096
|
function patternToRegex(pattern) {
|
|
635
1097
|
return new RegExp(pattern.replace(/\*/g, ".*"), "i");
|
|
636
1098
|
}
|
|
@@ -643,7 +1105,7 @@ async function searchFiles(searchPath, pattern, maxDepth) {
|
|
|
643
1105
|
const files = await api.withPromise();
|
|
644
1106
|
if (pattern) {
|
|
645
1107
|
const regex = patternToRegex(pattern);
|
|
646
|
-
return files.filter((file) => regex.test(
|
|
1108
|
+
return files.filter((file) => regex.test(path9.basename(file)));
|
|
647
1109
|
}
|
|
648
1110
|
return files;
|
|
649
1111
|
}
|
|
@@ -654,12 +1116,12 @@ async function searchFilesStream(searchPath, pattern, onResults, maxResults = 10
|
|
|
654
1116
|
async function searchDir(dirPath) {
|
|
655
1117
|
if (stopped) return;
|
|
656
1118
|
try {
|
|
657
|
-
const entries = await
|
|
1119
|
+
const entries = await fs8.readdir(dirPath, { withFileTypes: true });
|
|
658
1120
|
const matched = [];
|
|
659
1121
|
const subdirs = [];
|
|
660
1122
|
for (const entry of entries) {
|
|
661
1123
|
if (stopped) return;
|
|
662
|
-
const fullPath =
|
|
1124
|
+
const fullPath = path9.join(dirPath, entry.name);
|
|
663
1125
|
if (regex.test(entry.name)) {
|
|
664
1126
|
matched.push(fullPath);
|
|
665
1127
|
results.push(fullPath);
|
|
@@ -696,44 +1158,29 @@ function searchFilesSync(searchPath, pattern, maxDepth) {
|
|
|
696
1158
|
const files = api.sync();
|
|
697
1159
|
if (pattern) {
|
|
698
1160
|
const regex = new RegExp(pattern.replace(/\*/g, ".*"), "i");
|
|
699
|
-
return files.filter((file) => regex.test(
|
|
1161
|
+
return files.filter((file) => regex.test(path9.basename(file)));
|
|
700
1162
|
}
|
|
701
1163
|
return files;
|
|
702
1164
|
}
|
|
703
1165
|
|
|
704
1166
|
// src/hash.ts
|
|
705
|
-
import {
|
|
1167
|
+
import { xxhash64 } from "hash-wasm";
|
|
706
1168
|
import { stat } from "fs/promises";
|
|
707
|
-
async function getFileHash(filePath) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return createHash("md5").update(hashInput).digest("hex");
|
|
712
|
-
} catch (error) {
|
|
713
|
-
console.error(`Error computing hash for ${filePath}:`, error);
|
|
714
|
-
return createHash("md5").update(filePath).digest("hex");
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
async function getFileHashes(filePaths) {
|
|
718
|
-
const hashMap = /* @__PURE__ */ new Map();
|
|
719
|
-
await Promise.allSettled(
|
|
720
|
-
filePaths.map(async (filePath) => {
|
|
721
|
-
const hash = await getFileHash(filePath);
|
|
722
|
-
hashMap.set(filePath, hash);
|
|
723
|
-
})
|
|
724
|
-
);
|
|
725
|
-
return hashMap;
|
|
1169
|
+
async function getFileHash(filePath, stats) {
|
|
1170
|
+
const fileStats = stats || await stat(filePath);
|
|
1171
|
+
const hashInput = `${filePath}:${fileStats.size}:${fileStats.mtime.getTime()}`;
|
|
1172
|
+
return await xxhash64(hashInput);
|
|
726
1173
|
}
|
|
727
1174
|
|
|
728
1175
|
// src/clipboard.ts
|
|
729
|
-
import { promises as
|
|
730
|
-
import
|
|
1176
|
+
import { promises as fs9 } from "fs";
|
|
1177
|
+
import path10 from "path";
|
|
731
1178
|
async function copyFilesToClipboard(filePaths, clipboard) {
|
|
732
1179
|
try {
|
|
733
1180
|
const cleanPaths = [];
|
|
734
1181
|
for (const p of filePaths) {
|
|
735
1182
|
try {
|
|
736
|
-
await
|
|
1183
|
+
await fs9.access(p);
|
|
737
1184
|
cleanPaths.push(p);
|
|
738
1185
|
} catch {
|
|
739
1186
|
}
|
|
@@ -768,24 +1215,24 @@ async function pasteFiles(targetDir, sourcePaths) {
|
|
|
768
1215
|
}
|
|
769
1216
|
const pastedPaths = [];
|
|
770
1217
|
for (const sourcePath of sourcePaths) {
|
|
771
|
-
const fileName =
|
|
772
|
-
let destPath =
|
|
1218
|
+
const fileName = path10.basename(sourcePath);
|
|
1219
|
+
let destPath = path10.join(targetDir, fileName);
|
|
773
1220
|
let counter = 1;
|
|
774
1221
|
while (true) {
|
|
775
1222
|
try {
|
|
776
|
-
await
|
|
777
|
-
const ext =
|
|
778
|
-
const baseName =
|
|
779
|
-
destPath =
|
|
1223
|
+
await fs9.access(destPath);
|
|
1224
|
+
const ext = path10.extname(fileName);
|
|
1225
|
+
const baseName = path10.basename(fileName, ext);
|
|
1226
|
+
destPath = path10.join(targetDir, `${baseName} ${++counter}${ext}`);
|
|
780
1227
|
} catch {
|
|
781
1228
|
break;
|
|
782
1229
|
}
|
|
783
1230
|
}
|
|
784
|
-
const stats = await
|
|
1231
|
+
const stats = await fs9.stat(sourcePath);
|
|
785
1232
|
if (stats.isDirectory()) {
|
|
786
1233
|
await copyDirectory2(sourcePath, destPath);
|
|
787
1234
|
} else {
|
|
788
|
-
await
|
|
1235
|
+
await fs9.copyFile(sourcePath, destPath);
|
|
789
1236
|
}
|
|
790
1237
|
pastedPaths.push(destPath);
|
|
791
1238
|
}
|
|
@@ -795,28 +1242,28 @@ async function pasteFiles(targetDir, sourcePaths) {
|
|
|
795
1242
|
}
|
|
796
1243
|
}
|
|
797
1244
|
async function copyDirectory2(source, dest) {
|
|
798
|
-
await
|
|
799
|
-
const entries = await
|
|
1245
|
+
await fs9.mkdir(dest, { recursive: true });
|
|
1246
|
+
const entries = await fs9.readdir(source, { withFileTypes: true });
|
|
800
1247
|
for (const entry of entries) {
|
|
801
|
-
const sourcePath =
|
|
802
|
-
const destPath =
|
|
1248
|
+
const sourcePath = path10.join(source, entry.name);
|
|
1249
|
+
const destPath = path10.join(dest, entry.name);
|
|
803
1250
|
if (entry.isDirectory()) {
|
|
804
1251
|
await copyDirectory2(sourcePath, destPath);
|
|
805
1252
|
} else {
|
|
806
|
-
await
|
|
1253
|
+
await fs9.copyFile(sourcePath, destPath);
|
|
807
1254
|
}
|
|
808
1255
|
}
|
|
809
1256
|
}
|
|
810
1257
|
|
|
811
1258
|
// src/application-icon.ts
|
|
812
|
-
import { promises as
|
|
813
|
-
import
|
|
1259
|
+
import { promises as fs10 } from "fs";
|
|
1260
|
+
import path11 from "path";
|
|
814
1261
|
import os2 from "os";
|
|
815
|
-
import { exec } from "child_process";
|
|
816
|
-
import { promisify } from "util";
|
|
817
|
-
var
|
|
1262
|
+
import { exec as exec2 } from "child_process";
|
|
1263
|
+
import { promisify as promisify2 } from "util";
|
|
1264
|
+
var execAsync2 = promisify2(exec2);
|
|
818
1265
|
async function findAppIconPath(appPath) {
|
|
819
|
-
const resourcesPath =
|
|
1266
|
+
const resourcesPath = path11.join(appPath, "Contents", "Resources");
|
|
820
1267
|
try {
|
|
821
1268
|
const commonIconNames = [
|
|
822
1269
|
"AppIcon.icns",
|
|
@@ -824,18 +1271,18 @@ async function findAppIconPath(appPath) {
|
|
|
824
1271
|
"application.icns",
|
|
825
1272
|
"icon.icns"
|
|
826
1273
|
];
|
|
827
|
-
const infoPlistPath =
|
|
1274
|
+
const infoPlistPath = path11.join(appPath, "Contents", "Info.plist");
|
|
828
1275
|
try {
|
|
829
|
-
const infoPlistContent = await
|
|
1276
|
+
const infoPlistContent = await fs10.readFile(infoPlistPath, "utf-8");
|
|
830
1277
|
const iconFileMatch = infoPlistContent.match(/<key>CFBundleIconFile<\/key>\s*<string>([^<]+)<\/string>/);
|
|
831
1278
|
if (iconFileMatch && iconFileMatch[1]) {
|
|
832
1279
|
let iconFileName = iconFileMatch[1].trim();
|
|
833
1280
|
if (!iconFileName.endsWith(".icns")) {
|
|
834
1281
|
iconFileName += ".icns";
|
|
835
1282
|
}
|
|
836
|
-
const iconPath =
|
|
1283
|
+
const iconPath = path11.join(resourcesPath, iconFileName);
|
|
837
1284
|
try {
|
|
838
|
-
await
|
|
1285
|
+
await fs10.access(iconPath);
|
|
839
1286
|
return iconPath;
|
|
840
1287
|
} catch {
|
|
841
1288
|
}
|
|
@@ -843,19 +1290,19 @@ async function findAppIconPath(appPath) {
|
|
|
843
1290
|
} catch {
|
|
844
1291
|
}
|
|
845
1292
|
for (const iconName of commonIconNames) {
|
|
846
|
-
const iconPath =
|
|
1293
|
+
const iconPath = path11.join(resourcesPath, iconName);
|
|
847
1294
|
try {
|
|
848
|
-
await
|
|
1295
|
+
await fs10.access(iconPath);
|
|
849
1296
|
return iconPath;
|
|
850
1297
|
} catch {
|
|
851
1298
|
continue;
|
|
852
1299
|
}
|
|
853
1300
|
}
|
|
854
1301
|
try {
|
|
855
|
-
const entries = await
|
|
1302
|
+
const entries = await fs10.readdir(resourcesPath);
|
|
856
1303
|
const icnsFile = entries.find((entry) => entry.toLowerCase().endsWith(".icns"));
|
|
857
1304
|
if (icnsFile) {
|
|
858
|
-
return
|
|
1305
|
+
return path11.join(resourcesPath, icnsFile);
|
|
859
1306
|
}
|
|
860
1307
|
} catch {
|
|
861
1308
|
}
|
|
@@ -869,7 +1316,7 @@ async function getApplicationIcon(appPath) {
|
|
|
869
1316
|
return null;
|
|
870
1317
|
}
|
|
871
1318
|
try {
|
|
872
|
-
const stats = await
|
|
1319
|
+
const stats = await fs10.stat(appPath);
|
|
873
1320
|
if (!stats.isDirectory() || !appPath.endsWith(".app")) {
|
|
874
1321
|
return null;
|
|
875
1322
|
}
|
|
@@ -881,13 +1328,13 @@ async function getApplicationIcon(appPath) {
|
|
|
881
1328
|
return null;
|
|
882
1329
|
}
|
|
883
1330
|
try {
|
|
884
|
-
const tempPngPath =
|
|
885
|
-
await
|
|
1331
|
+
const tempPngPath = path11.join(os2.tmpdir(), `app-icon-${Date.now()}.png`);
|
|
1332
|
+
await execAsync2(
|
|
886
1333
|
`sips -s format png "${iconPath}" --out "${tempPngPath}" --resampleHeightWidthMax 128`
|
|
887
1334
|
);
|
|
888
|
-
const pngBuffer = await
|
|
1335
|
+
const pngBuffer = await fs10.readFile(tempPngPath);
|
|
889
1336
|
try {
|
|
890
|
-
await
|
|
1337
|
+
await fs10.unlink(tempPngPath);
|
|
891
1338
|
} catch {
|
|
892
1339
|
}
|
|
893
1340
|
const base64 = pngBuffer.toString("base64");
|
|
@@ -897,11 +1344,150 @@ async function getApplicationIcon(appPath) {
|
|
|
897
1344
|
}
|
|
898
1345
|
}
|
|
899
1346
|
|
|
1347
|
+
// src/watch.ts
|
|
1348
|
+
import * as fs11 from "fs";
|
|
1349
|
+
import * as path12 from "path";
|
|
1350
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
1351
|
+
var DEBOUNCE_DELAY = 100;
|
|
1352
|
+
function watchDirectory(dirPath, callback) {
|
|
1353
|
+
let watcher = null;
|
|
1354
|
+
try {
|
|
1355
|
+
watcher = fs11.watch(dirPath, { persistent: true }, (eventType, filename) => {
|
|
1356
|
+
if (!filename) return;
|
|
1357
|
+
const fullPath = path12.join(dirPath, filename);
|
|
1358
|
+
const key = `${dirPath}:${filename}`;
|
|
1359
|
+
const existingTimer = debounceTimers.get(key);
|
|
1360
|
+
if (existingTimer) {
|
|
1361
|
+
clearTimeout(existingTimer);
|
|
1362
|
+
}
|
|
1363
|
+
const timer = setTimeout(() => {
|
|
1364
|
+
debounceTimers.delete(key);
|
|
1365
|
+
fs11.access(fullPath, fs11.constants.F_OK, (err) => {
|
|
1366
|
+
let type;
|
|
1367
|
+
if (err) {
|
|
1368
|
+
type = "remove";
|
|
1369
|
+
} else if (eventType === "rename") {
|
|
1370
|
+
type = "add";
|
|
1371
|
+
} else {
|
|
1372
|
+
type = "change";
|
|
1373
|
+
}
|
|
1374
|
+
callback({
|
|
1375
|
+
type,
|
|
1376
|
+
path: fullPath,
|
|
1377
|
+
filename
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
}, DEBOUNCE_DELAY);
|
|
1381
|
+
debounceTimers.set(key, timer);
|
|
1382
|
+
});
|
|
1383
|
+
watcher.on("error", (error) => {
|
|
1384
|
+
console.error("Watch error:", error);
|
|
1385
|
+
});
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
console.error("Failed to watch directory:", error);
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
close: () => {
|
|
1391
|
+
if (watcher) {
|
|
1392
|
+
watcher.close();
|
|
1393
|
+
watcher = null;
|
|
1394
|
+
}
|
|
1395
|
+
for (const [key, timer] of debounceTimers.entries()) {
|
|
1396
|
+
if (key.startsWith(`${dirPath}:`)) {
|
|
1397
|
+
clearTimeout(timer);
|
|
1398
|
+
debounceTimers.delete(key);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
path: dirPath
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
var WatchManager = class {
|
|
1406
|
+
watchers = /* @__PURE__ */ new Map();
|
|
1407
|
+
callbacks = /* @__PURE__ */ new Map();
|
|
1408
|
+
/**
|
|
1409
|
+
* 开始监听目录
|
|
1410
|
+
*/
|
|
1411
|
+
watch(dirPath, callback) {
|
|
1412
|
+
const normalizedPath = path12.normalize(dirPath);
|
|
1413
|
+
let callbackSet = this.callbacks.get(normalizedPath);
|
|
1414
|
+
if (!callbackSet) {
|
|
1415
|
+
callbackSet = /* @__PURE__ */ new Set();
|
|
1416
|
+
this.callbacks.set(normalizedPath, callbackSet);
|
|
1417
|
+
}
|
|
1418
|
+
callbackSet.add(callback);
|
|
1419
|
+
let watcherInfo = this.watchers.get(normalizedPath);
|
|
1420
|
+
if (watcherInfo) {
|
|
1421
|
+
watcherInfo.refCount++;
|
|
1422
|
+
} else {
|
|
1423
|
+
const watcher = watchDirectory(normalizedPath, (event) => {
|
|
1424
|
+
const callbacks = this.callbacks.get(normalizedPath);
|
|
1425
|
+
if (callbacks) {
|
|
1426
|
+
for (const cb of callbacks) {
|
|
1427
|
+
try {
|
|
1428
|
+
cb(event);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
console.error("Watch callback error:", error);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
watcherInfo = { watcher, refCount: 1 };
|
|
1436
|
+
this.watchers.set(normalizedPath, watcherInfo);
|
|
1437
|
+
}
|
|
1438
|
+
return () => {
|
|
1439
|
+
this.unwatch(normalizedPath, callback);
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* 停止监听
|
|
1444
|
+
*/
|
|
1445
|
+
unwatch(dirPath, callback) {
|
|
1446
|
+
const normalizedPath = path12.normalize(dirPath);
|
|
1447
|
+
const callbackSet = this.callbacks.get(normalizedPath);
|
|
1448
|
+
if (callbackSet) {
|
|
1449
|
+
callbackSet.delete(callback);
|
|
1450
|
+
if (callbackSet.size === 0) {
|
|
1451
|
+
this.callbacks.delete(normalizedPath);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
const watcherInfo = this.watchers.get(normalizedPath);
|
|
1455
|
+
if (watcherInfo) {
|
|
1456
|
+
watcherInfo.refCount--;
|
|
1457
|
+
if (watcherInfo.refCount <= 0) {
|
|
1458
|
+
watcherInfo.watcher.close();
|
|
1459
|
+
this.watchers.delete(normalizedPath);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* 关闭所有监听器
|
|
1465
|
+
*/
|
|
1466
|
+
closeAll() {
|
|
1467
|
+
for (const [, watcherInfo] of this.watchers) {
|
|
1468
|
+
watcherInfo.watcher.close();
|
|
1469
|
+
}
|
|
1470
|
+
this.watchers.clear();
|
|
1471
|
+
this.callbacks.clear();
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
var globalWatchManager = null;
|
|
1475
|
+
function getWatchManager() {
|
|
1476
|
+
if (!globalWatchManager) {
|
|
1477
|
+
globalWatchManager = new WatchManager();
|
|
1478
|
+
}
|
|
1479
|
+
return globalWatchManager;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
900
1482
|
// src/thumbnail/service.ts
|
|
901
|
-
import { promises as
|
|
902
|
-
import
|
|
903
|
-
|
|
904
|
-
|
|
1483
|
+
import { promises as fs12 } from "fs";
|
|
1484
|
+
import path13 from "path";
|
|
1485
|
+
function isImageFile(filePath, fileType) {
|
|
1486
|
+
return fileType === "image" /* IMAGE */;
|
|
1487
|
+
}
|
|
1488
|
+
function isVideoFile(filePath, fileType) {
|
|
1489
|
+
return fileType === "video" /* VIDEO */;
|
|
1490
|
+
}
|
|
905
1491
|
var ThumbnailService = class {
|
|
906
1492
|
database;
|
|
907
1493
|
imageProcessor;
|
|
@@ -920,7 +1506,7 @@ var ThumbnailService = class {
|
|
|
920
1506
|
*/
|
|
921
1507
|
async getCachedThumbnailUrl(filePath) {
|
|
922
1508
|
try {
|
|
923
|
-
const stats = await
|
|
1509
|
+
const stats = await fs12.stat(filePath);
|
|
924
1510
|
const fileType = getFileType(filePath, stats);
|
|
925
1511
|
if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
|
|
926
1512
|
return await this.getApplicationIcon(filePath);
|
|
@@ -933,7 +1519,7 @@ var ThumbnailService = class {
|
|
|
933
1519
|
if (cachedPath) {
|
|
934
1520
|
return this.urlEncoder(cachedPath);
|
|
935
1521
|
}
|
|
936
|
-
getFileHash(filePath).then((fileHash) => {
|
|
1522
|
+
getFileHash(filePath, stats).then((fileHash) => {
|
|
937
1523
|
this.generateThumbnail(filePath, fileHash, mtime).catch(() => {
|
|
938
1524
|
});
|
|
939
1525
|
}).catch(() => {
|
|
@@ -948,7 +1534,7 @@ var ThumbnailService = class {
|
|
|
948
1534
|
*/
|
|
949
1535
|
async getThumbnailUrl(filePath) {
|
|
950
1536
|
try {
|
|
951
|
-
const stats = await
|
|
1537
|
+
const stats = await fs12.stat(filePath);
|
|
952
1538
|
const fileType = getFileType(filePath, stats);
|
|
953
1539
|
if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
|
|
954
1540
|
return await this.getApplicationIcon(filePath);
|
|
@@ -961,7 +1547,7 @@ var ThumbnailService = class {
|
|
|
961
1547
|
if (cachedPath) {
|
|
962
1548
|
return this.urlEncoder(cachedPath);
|
|
963
1549
|
}
|
|
964
|
-
const fileHash = await getFileHash(filePath);
|
|
1550
|
+
const fileHash = await getFileHash(filePath, stats);
|
|
965
1551
|
const thumbnailPath = await this.generateThumbnail(filePath, fileHash, mtime);
|
|
966
1552
|
if (thumbnailPath) {
|
|
967
1553
|
return this.urlEncoder(thumbnailPath);
|
|
@@ -981,13 +1567,14 @@ var ThumbnailService = class {
|
|
|
981
1567
|
return cachedPath;
|
|
982
1568
|
}
|
|
983
1569
|
try {
|
|
984
|
-
const
|
|
1570
|
+
const stats = await fs12.stat(filePath);
|
|
1571
|
+
const fileType = getFileType(filePath, stats);
|
|
985
1572
|
const hashPrefix = fileHash.substring(0, 16);
|
|
986
1573
|
const thumbnailFileName = `${hashPrefix}.jpg`;
|
|
987
|
-
const thumbnailPath =
|
|
988
|
-
if (
|
|
1574
|
+
const thumbnailPath = path13.join(this.database.getCacheDir(), thumbnailFileName);
|
|
1575
|
+
if (isImageFile(filePath, fileType) && this.imageProcessor) {
|
|
989
1576
|
await this.imageProcessor.resize(filePath, thumbnailPath, 256);
|
|
990
|
-
} else if (
|
|
1577
|
+
} else if (isVideoFile(filePath, fileType) && this.videoProcessor) {
|
|
991
1578
|
await this.videoProcessor.screenshot(filePath, thumbnailPath, "00:00:01", "256x256");
|
|
992
1579
|
} else {
|
|
993
1580
|
return null;
|
|
@@ -1000,12 +1587,20 @@ var ThumbnailService = class {
|
|
|
1000
1587
|
}
|
|
1001
1588
|
}
|
|
1002
1589
|
/**
|
|
1003
|
-
*
|
|
1590
|
+
* 批量生成缩略图(带并发限制,避免资源耗尽)
|
|
1004
1591
|
*/
|
|
1005
|
-
async generateThumbnailsBatch(files) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1592
|
+
async generateThumbnailsBatch(files, concurrency = 3) {
|
|
1593
|
+
const execute = async (file) => {
|
|
1594
|
+
try {
|
|
1595
|
+
await this.generateThumbnail(file.path, file.hash, file.mtime);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
console.debug(`Failed to generate thumbnail for ${file.path}:`, error);
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
for (let i = 0; i < files.length; i += concurrency) {
|
|
1601
|
+
const batch = files.slice(i, i + concurrency);
|
|
1602
|
+
await Promise.allSettled(batch.map(execute));
|
|
1603
|
+
}
|
|
1009
1604
|
}
|
|
1010
1605
|
/**
|
|
1011
1606
|
* 删除缩略图
|
|
@@ -1031,13 +1626,18 @@ function getThumbnailService() {
|
|
|
1031
1626
|
|
|
1032
1627
|
// src/thumbnail/database.ts
|
|
1033
1628
|
import Database from "better-sqlite3";
|
|
1034
|
-
import
|
|
1629
|
+
import path14 from "path";
|
|
1035
1630
|
import { existsSync, mkdirSync } from "fs";
|
|
1036
1631
|
var SqliteThumbnailDatabase = class {
|
|
1037
1632
|
db = null;
|
|
1038
1633
|
cacheDir;
|
|
1039
|
-
|
|
1040
|
-
|
|
1634
|
+
dbPath;
|
|
1635
|
+
constructor(userDataPath, options = {}) {
|
|
1636
|
+
const defaultDirName = options.dirName || "thumbnails";
|
|
1637
|
+
const defaultDbFileName = options.dbFileName || "thumbnails.db";
|
|
1638
|
+
const inferredDirFromDbPath = options.dbPath ? path14.dirname(options.dbPath) : null;
|
|
1639
|
+
this.cacheDir = options.thumbnailDir ? options.thumbnailDir : inferredDirFromDbPath || path14.join(userDataPath, defaultDirName);
|
|
1640
|
+
this.dbPath = options.dbPath ? options.dbPath : path14.join(this.cacheDir, defaultDbFileName);
|
|
1041
1641
|
if (!existsSync(this.cacheDir)) {
|
|
1042
1642
|
mkdirSync(this.cacheDir, { recursive: true });
|
|
1043
1643
|
}
|
|
@@ -1047,8 +1647,7 @@ var SqliteThumbnailDatabase = class {
|
|
|
1047
1647
|
*/
|
|
1048
1648
|
init() {
|
|
1049
1649
|
if (this.db) return;
|
|
1050
|
-
|
|
1051
|
-
this.db = new Database(dbPath, {
|
|
1650
|
+
this.db = new Database(this.dbPath, {
|
|
1052
1651
|
fileMustExist: false
|
|
1053
1652
|
});
|
|
1054
1653
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -1120,15 +1719,19 @@ var SqliteThumbnailDatabase = class {
|
|
|
1120
1719
|
}
|
|
1121
1720
|
close() {
|
|
1122
1721
|
if (this.db) {
|
|
1722
|
+
try {
|
|
1723
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
}
|
|
1123
1726
|
this.db.close();
|
|
1124
1727
|
this.db = null;
|
|
1125
1728
|
}
|
|
1126
1729
|
}
|
|
1127
1730
|
};
|
|
1128
1731
|
var thumbnailDb = null;
|
|
1129
|
-
function createSqliteThumbnailDatabase(
|
|
1732
|
+
function createSqliteThumbnailDatabase(options) {
|
|
1130
1733
|
if (!thumbnailDb) {
|
|
1131
|
-
thumbnailDb = new SqliteThumbnailDatabase(userDataPath);
|
|
1734
|
+
thumbnailDb = new SqliteThumbnailDatabase(options.userDataPath, options);
|
|
1132
1735
|
thumbnailDb.init();
|
|
1133
1736
|
}
|
|
1134
1737
|
return thumbnailDb;
|
|
@@ -1136,58 +1739,698 @@ function createSqliteThumbnailDatabase(userDataPath) {
|
|
|
1136
1739
|
function getSqliteThumbnailDatabase() {
|
|
1137
1740
|
return thumbnailDb;
|
|
1138
1741
|
}
|
|
1742
|
+
function closeThumbnailDatabase() {
|
|
1743
|
+
if (thumbnailDb) {
|
|
1744
|
+
thumbnailDb.close();
|
|
1745
|
+
thumbnailDb = null;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1139
1748
|
|
|
1140
1749
|
// src/thumbnail/processors.ts
|
|
1141
|
-
import {
|
|
1142
|
-
import { promisify as promisify2 } from "util";
|
|
1143
|
-
var execFileAsync = promisify2(execFile);
|
|
1750
|
+
import { spawn } from "child_process";
|
|
1144
1751
|
function createSharpImageProcessor(sharp) {
|
|
1145
1752
|
return {
|
|
1146
1753
|
async resize(filePath, outputPath, size) {
|
|
1147
|
-
await sharp(filePath).resize(
|
|
1148
|
-
|
|
1754
|
+
await sharp(filePath).resize({
|
|
1755
|
+
width: size,
|
|
1149
1756
|
withoutEnlargement: true
|
|
1150
|
-
}).jpeg({
|
|
1757
|
+
}).jpeg({
|
|
1758
|
+
quality: 80,
|
|
1759
|
+
optimiseCoding: true
|
|
1760
|
+
// 优化编码,提升压缩率
|
|
1761
|
+
}).toFile(outputPath);
|
|
1151
1762
|
}
|
|
1152
1763
|
};
|
|
1153
1764
|
}
|
|
1154
1765
|
function createFfmpegVideoProcessor(ffmpegPath) {
|
|
1155
1766
|
return {
|
|
1156
1767
|
async screenshot(filePath, outputPath, timestamp, size) {
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1768
|
+
const width = size.split("x")[0];
|
|
1769
|
+
return new Promise((resolve, reject) => {
|
|
1770
|
+
const ffmpeg = spawn(ffmpegPath, [
|
|
1771
|
+
"-y",
|
|
1772
|
+
"-ss",
|
|
1773
|
+
timestamp,
|
|
1774
|
+
"-i",
|
|
1775
|
+
filePath,
|
|
1776
|
+
"-vframes",
|
|
1777
|
+
"1",
|
|
1778
|
+
"-vf",
|
|
1779
|
+
`thumbnail,scale=${width}:-1:force_original_aspect_ratio=decrease`,
|
|
1780
|
+
// 使用 mjpeg 编码器,性能更好(借鉴 pixflow)
|
|
1781
|
+
"-c:v",
|
|
1782
|
+
"mjpeg",
|
|
1783
|
+
"-q:v",
|
|
1784
|
+
"6",
|
|
1785
|
+
// 质量参数,6 表示中等质量(范围 2-31,数值越小质量越高)
|
|
1786
|
+
outputPath
|
|
1787
|
+
]);
|
|
1788
|
+
const timeout = setTimeout(() => {
|
|
1789
|
+
ffmpeg.kill();
|
|
1790
|
+
reject(new Error("Video thumbnail generation timeout"));
|
|
1791
|
+
}, 3e4);
|
|
1792
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
1793
|
+
const output = data.toString();
|
|
1794
|
+
if (output.includes("Unsupported pixel format")) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
ffmpeg.on("close", (code) => {
|
|
1799
|
+
clearTimeout(timeout);
|
|
1800
|
+
if (code === 0) {
|
|
1801
|
+
resolve();
|
|
1802
|
+
} else {
|
|
1803
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
ffmpeg.on("error", (error) => {
|
|
1807
|
+
clearTimeout(timeout);
|
|
1808
|
+
reject(error);
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1170
1811
|
}
|
|
1171
1812
|
};
|
|
1172
1813
|
}
|
|
1814
|
+
|
|
1815
|
+
// src/media/format-detector.ts
|
|
1816
|
+
import { execFile } from "child_process";
|
|
1817
|
+
import { promisify as promisify3 } from "util";
|
|
1818
|
+
import path15 from "path";
|
|
1819
|
+
var execFileAsync = promisify3(execFile);
|
|
1820
|
+
var BROWSER_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["mp4", "webm", "ogg", "ogv"]);
|
|
1821
|
+
var BROWSER_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "vp8", "vp9", "theora", "av1"]);
|
|
1822
|
+
var BROWSER_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "mp3", "opus", "vorbis", "flac"]);
|
|
1823
|
+
var BROWSER_AUDIO_CONTAINERS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "oga", "webm", "m4a", "aac", "flac"]);
|
|
1824
|
+
var BROWSER_AUDIO_ONLY_CODECS = /* @__PURE__ */ new Set(["mp3", "aac", "opus", "vorbis", "flac", "pcm_s16le", "pcm_s24le"]);
|
|
1825
|
+
var REMUXABLE_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "hevc", "h265"]);
|
|
1826
|
+
var REMUXABLE_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "alac"]);
|
|
1827
|
+
var VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
1828
|
+
"mp4",
|
|
1829
|
+
"mkv",
|
|
1830
|
+
"avi",
|
|
1831
|
+
"mov",
|
|
1832
|
+
"wmv",
|
|
1833
|
+
"flv",
|
|
1834
|
+
"webm",
|
|
1835
|
+
"ogv",
|
|
1836
|
+
"ogg",
|
|
1837
|
+
"m4v",
|
|
1838
|
+
"mpeg",
|
|
1839
|
+
"mpg",
|
|
1840
|
+
"3gp",
|
|
1841
|
+
"ts",
|
|
1842
|
+
"mts",
|
|
1843
|
+
"m2ts",
|
|
1844
|
+
"vob",
|
|
1845
|
+
"rmvb",
|
|
1846
|
+
"rm"
|
|
1847
|
+
]);
|
|
1848
|
+
var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1849
|
+
"mp3",
|
|
1850
|
+
"wav",
|
|
1851
|
+
"flac",
|
|
1852
|
+
"aac",
|
|
1853
|
+
"m4a",
|
|
1854
|
+
"ogg",
|
|
1855
|
+
"oga",
|
|
1856
|
+
"wma",
|
|
1857
|
+
"ape",
|
|
1858
|
+
"alac",
|
|
1859
|
+
"aiff",
|
|
1860
|
+
"aif",
|
|
1861
|
+
"opus",
|
|
1862
|
+
"mid",
|
|
1863
|
+
"midi",
|
|
1864
|
+
"wv",
|
|
1865
|
+
"mka"
|
|
1866
|
+
]);
|
|
1867
|
+
function getMediaTypeByExtension(filePath) {
|
|
1868
|
+
const ext = path15.extname(filePath).toLowerCase().slice(1);
|
|
1869
|
+
if (VIDEO_EXTENSIONS2.has(ext)) return "video";
|
|
1870
|
+
if (AUDIO_EXTENSIONS.has(ext)) return "audio";
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
async function getMediaFormat(filePath, ffprobePath) {
|
|
1874
|
+
try {
|
|
1875
|
+
const { stdout } = await execFileAsync(ffprobePath, [
|
|
1876
|
+
"-v",
|
|
1877
|
+
"quiet",
|
|
1878
|
+
"-print_format",
|
|
1879
|
+
"json",
|
|
1880
|
+
"-show_format",
|
|
1881
|
+
"-show_streams",
|
|
1882
|
+
filePath
|
|
1883
|
+
]);
|
|
1884
|
+
const data = JSON.parse(stdout);
|
|
1885
|
+
const format = data.format || {};
|
|
1886
|
+
const streams = data.streams || [];
|
|
1887
|
+
const videoStream = streams.find((s) => s.codec_type === "video");
|
|
1888
|
+
const audioStream = streams.find((s) => s.codec_type === "audio");
|
|
1889
|
+
const type = videoStream ? "video" : "audio";
|
|
1890
|
+
const formatName = format.format_name || "";
|
|
1891
|
+
const container = formatName.split(",")[0].toLowerCase();
|
|
1892
|
+
return {
|
|
1893
|
+
type,
|
|
1894
|
+
container,
|
|
1895
|
+
videoCodec: videoStream?.codec_name?.toLowerCase(),
|
|
1896
|
+
audioCodec: audioStream?.codec_name?.toLowerCase(),
|
|
1897
|
+
duration: parseFloat(format.duration) || void 0,
|
|
1898
|
+
width: videoStream?.width,
|
|
1899
|
+
height: videoStream?.height,
|
|
1900
|
+
bitrate: parseInt(format.bit_rate) || void 0
|
|
1901
|
+
};
|
|
1902
|
+
} catch {
|
|
1903
|
+
const type = getMediaTypeByExtension(filePath);
|
|
1904
|
+
if (!type) return null;
|
|
1905
|
+
const ext = path15.extname(filePath).toLowerCase().slice(1);
|
|
1906
|
+
return {
|
|
1907
|
+
type,
|
|
1908
|
+
container: ext
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function canPlayVideoDirectly(format) {
|
|
1913
|
+
if (!BROWSER_VIDEO_CONTAINERS.has(format.container)) {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
if (format.videoCodec && !BROWSER_VIDEO_CODECS.has(format.videoCodec)) {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
if (format.audioCodec && !BROWSER_AUDIO_CODECS.has(format.audioCodec)) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
function canPlayAudioDirectly(format) {
|
|
1925
|
+
if (!BROWSER_AUDIO_CONTAINERS.has(format.container)) {
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
if (format.audioCodec && !BROWSER_AUDIO_ONLY_CODECS.has(format.audioCodec)) {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
function canRemuxVideo(format) {
|
|
1934
|
+
if (!format.videoCodec || !REMUXABLE_VIDEO_CODECS.has(format.videoCodec)) {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
if (format.audioCodec) {
|
|
1938
|
+
const audioOk = BROWSER_AUDIO_CODECS.has(format.audioCodec) || REMUXABLE_AUDIO_CODECS.has(format.audioCodec);
|
|
1939
|
+
if (!audioOk) {
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1945
|
+
function canRemuxAudio(format) {
|
|
1946
|
+
return format.audioCodec ? REMUXABLE_AUDIO_CODECS.has(format.audioCodec) : false;
|
|
1947
|
+
}
|
|
1948
|
+
function estimateTranscodeTime(duration, method) {
|
|
1949
|
+
if (!duration || method === "direct") return void 0;
|
|
1950
|
+
if (method === "remux") {
|
|
1951
|
+
return Math.ceil(duration / 50);
|
|
1952
|
+
}
|
|
1953
|
+
return Math.ceil(duration / 3);
|
|
1954
|
+
}
|
|
1955
|
+
async function detectTranscodeNeeds(filePath, ffprobePath) {
|
|
1956
|
+
const formatInfo = await getMediaFormat(filePath, ffprobePath);
|
|
1957
|
+
if (!formatInfo) {
|
|
1958
|
+
const type2 = getMediaTypeByExtension(filePath) || "video";
|
|
1959
|
+
return {
|
|
1960
|
+
type: type2,
|
|
1961
|
+
needsTranscode: false,
|
|
1962
|
+
method: "direct"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
const { type } = formatInfo;
|
|
1966
|
+
if (type === "video") {
|
|
1967
|
+
if (canPlayVideoDirectly(formatInfo)) {
|
|
1968
|
+
return {
|
|
1969
|
+
type,
|
|
1970
|
+
needsTranscode: false,
|
|
1971
|
+
method: "direct",
|
|
1972
|
+
formatInfo
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (canRemuxVideo(formatInfo)) {
|
|
1976
|
+
return {
|
|
1977
|
+
type,
|
|
1978
|
+
needsTranscode: true,
|
|
1979
|
+
method: "remux",
|
|
1980
|
+
formatInfo,
|
|
1981
|
+
targetFormat: "mp4",
|
|
1982
|
+
estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
return {
|
|
1986
|
+
type,
|
|
1987
|
+
needsTranscode: true,
|
|
1988
|
+
method: "transcode",
|
|
1989
|
+
formatInfo,
|
|
1990
|
+
targetFormat: "mp4",
|
|
1991
|
+
estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (canPlayAudioDirectly(formatInfo)) {
|
|
1995
|
+
return {
|
|
1996
|
+
type,
|
|
1997
|
+
needsTranscode: false,
|
|
1998
|
+
method: "direct",
|
|
1999
|
+
formatInfo
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
if (canRemuxAudio(formatInfo)) {
|
|
2003
|
+
return {
|
|
2004
|
+
type,
|
|
2005
|
+
needsTranscode: true,
|
|
2006
|
+
method: "remux",
|
|
2007
|
+
formatInfo,
|
|
2008
|
+
targetFormat: "m4a",
|
|
2009
|
+
estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
type,
|
|
2014
|
+
needsTranscode: true,
|
|
2015
|
+
method: "transcode",
|
|
2016
|
+
formatInfo,
|
|
2017
|
+
targetFormat: "mp3",
|
|
2018
|
+
estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/media/transcoder.ts
|
|
2023
|
+
import { spawn as spawn2 } from "child_process";
|
|
2024
|
+
import fs13 from "fs/promises";
|
|
2025
|
+
import path16 from "path";
|
|
2026
|
+
import os3 from "os";
|
|
2027
|
+
async function getTempOutputPath(sourceFile, targetFormat, tempDir) {
|
|
2028
|
+
const dir = tempDir || path16.join(os3.tmpdir(), "file-explorer-media");
|
|
2029
|
+
await fs13.mkdir(dir, { recursive: true });
|
|
2030
|
+
const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
|
|
2031
|
+
const timestamp = Date.now();
|
|
2032
|
+
const outputName = `${baseName}_${timestamp}.${targetFormat}`;
|
|
2033
|
+
return path16.join(dir, outputName);
|
|
2034
|
+
}
|
|
2035
|
+
function parseProgress(stderr, duration) {
|
|
2036
|
+
const timeMatch = stderr.match(/time=(\d+):(\d+):(\d+)\.(\d+)/);
|
|
2037
|
+
if (!timeMatch) return null;
|
|
2038
|
+
const hours = parseInt(timeMatch[1]);
|
|
2039
|
+
const minutes = parseInt(timeMatch[2]);
|
|
2040
|
+
const seconds = parseInt(timeMatch[3]);
|
|
2041
|
+
const ms = parseInt(timeMatch[4]);
|
|
2042
|
+
const currentTime = hours * 3600 + minutes * 60 + seconds + ms / 100;
|
|
2043
|
+
const speedMatch = stderr.match(/speed=\s*([\d.]+)x/);
|
|
2044
|
+
const speed = speedMatch ? `${speedMatch[1]}x` : void 0;
|
|
2045
|
+
let percent = 0;
|
|
2046
|
+
if (duration && duration > 0) {
|
|
2047
|
+
percent = Math.min(100, Math.round(currentTime / duration * 100));
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
percent,
|
|
2051
|
+
time: currentTime,
|
|
2052
|
+
duration,
|
|
2053
|
+
speed
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
async function remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
|
|
2057
|
+
return new Promise((resolve, reject) => {
|
|
2058
|
+
const args = [
|
|
2059
|
+
"-i",
|
|
2060
|
+
inputPath,
|
|
2061
|
+
"-c",
|
|
2062
|
+
"copy",
|
|
2063
|
+
// 复制流,不重新编码
|
|
2064
|
+
"-movflags",
|
|
2065
|
+
"+faststart",
|
|
2066
|
+
// 优化 MP4 播放
|
|
2067
|
+
"-y",
|
|
2068
|
+
// 覆盖输出文件
|
|
2069
|
+
outputPath
|
|
2070
|
+
];
|
|
2071
|
+
const ffmpeg = spawn2(ffmpegPath, args);
|
|
2072
|
+
let stderrBuffer = "";
|
|
2073
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
2074
|
+
stderrBuffer += data.toString();
|
|
2075
|
+
if (onProgress) {
|
|
2076
|
+
const progress = parseProgress(stderrBuffer, duration);
|
|
2077
|
+
if (progress) {
|
|
2078
|
+
onProgress(progress);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
ffmpeg.on("close", (code) => {
|
|
2083
|
+
if (code === 0) {
|
|
2084
|
+
resolve();
|
|
2085
|
+
} else {
|
|
2086
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
ffmpeg.on("error", reject);
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
async function transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
|
|
2093
|
+
return new Promise((resolve, reject) => {
|
|
2094
|
+
const args = [
|
|
2095
|
+
"-i",
|
|
2096
|
+
inputPath,
|
|
2097
|
+
"-c:v",
|
|
2098
|
+
"libx264",
|
|
2099
|
+
// H.264 编码
|
|
2100
|
+
"-preset",
|
|
2101
|
+
"fast",
|
|
2102
|
+
// 编码速度预设
|
|
2103
|
+
"-crf",
|
|
2104
|
+
"23",
|
|
2105
|
+
// 质量(18-28,越小越好)
|
|
2106
|
+
"-c:a",
|
|
2107
|
+
"aac",
|
|
2108
|
+
// AAC 音频
|
|
2109
|
+
"-b:a",
|
|
2110
|
+
"192k",
|
|
2111
|
+
// 音频比特率
|
|
2112
|
+
"-movflags",
|
|
2113
|
+
"+faststart",
|
|
2114
|
+
"-y",
|
|
2115
|
+
outputPath
|
|
2116
|
+
];
|
|
2117
|
+
const ffmpeg = spawn2(ffmpegPath, args);
|
|
2118
|
+
let stderrBuffer = "";
|
|
2119
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
2120
|
+
stderrBuffer += data.toString();
|
|
2121
|
+
if (onProgress) {
|
|
2122
|
+
const progress = parseProgress(stderrBuffer, duration);
|
|
2123
|
+
if (progress) {
|
|
2124
|
+
onProgress(progress);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
ffmpeg.on("close", (code) => {
|
|
2129
|
+
if (code === 0) {
|
|
2130
|
+
resolve();
|
|
2131
|
+
} else {
|
|
2132
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
ffmpeg.on("error", reject);
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
async function remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
|
|
2139
|
+
return new Promise((resolve, reject) => {
|
|
2140
|
+
const args = [
|
|
2141
|
+
"-i",
|
|
2142
|
+
inputPath,
|
|
2143
|
+
"-c",
|
|
2144
|
+
"copy",
|
|
2145
|
+
"-y",
|
|
2146
|
+
outputPath
|
|
2147
|
+
];
|
|
2148
|
+
const ffmpeg = spawn2(ffmpegPath, args);
|
|
2149
|
+
let stderrBuffer = "";
|
|
2150
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
2151
|
+
stderrBuffer += data.toString();
|
|
2152
|
+
if (onProgress) {
|
|
2153
|
+
const progress = parseProgress(stderrBuffer, duration);
|
|
2154
|
+
if (progress) {
|
|
2155
|
+
onProgress(progress);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
ffmpeg.on("close", (code) => {
|
|
2160
|
+
if (code === 0) {
|
|
2161
|
+
resolve();
|
|
2162
|
+
} else {
|
|
2163
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
ffmpeg.on("error", reject);
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
async function transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
|
|
2170
|
+
return new Promise((resolve, reject) => {
|
|
2171
|
+
const ext = path16.extname(outputPath).toLowerCase();
|
|
2172
|
+
const isM4a = ext === ".m4a";
|
|
2173
|
+
const args = [
|
|
2174
|
+
"-i",
|
|
2175
|
+
inputPath,
|
|
2176
|
+
"-c:a",
|
|
2177
|
+
isM4a ? "aac" : "libmp3lame",
|
|
2178
|
+
"-b:a",
|
|
2179
|
+
"192k",
|
|
2180
|
+
"-y",
|
|
2181
|
+
outputPath
|
|
2182
|
+
];
|
|
2183
|
+
const ffmpeg = spawn2(ffmpegPath, args);
|
|
2184
|
+
let stderrBuffer = "";
|
|
2185
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
2186
|
+
stderrBuffer += data.toString();
|
|
2187
|
+
if (onProgress) {
|
|
2188
|
+
const progress = parseProgress(stderrBuffer, duration);
|
|
2189
|
+
if (progress) {
|
|
2190
|
+
onProgress(progress);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
ffmpeg.on("close", (code) => {
|
|
2195
|
+
if (code === 0) {
|
|
2196
|
+
resolve();
|
|
2197
|
+
} else {
|
|
2198
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
ffmpeg.on("error", reject);
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
async function transcodeMedia(ffmpegPath, inputPath, transcodeInfo, tempDir, onProgress) {
|
|
2205
|
+
try {
|
|
2206
|
+
if (!transcodeInfo.needsTranscode) {
|
|
2207
|
+
return {
|
|
2208
|
+
success: true,
|
|
2209
|
+
outputPath: inputPath
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
const targetFormat = transcodeInfo.targetFormat || (transcodeInfo.type === "video" ? "mp4" : "mp3");
|
|
2213
|
+
const outputPath = await getTempOutputPath(inputPath, targetFormat, tempDir);
|
|
2214
|
+
const duration = transcodeInfo.formatInfo?.duration;
|
|
2215
|
+
if (transcodeInfo.type === "video") {
|
|
2216
|
+
if (transcodeInfo.method === "remux") {
|
|
2217
|
+
await remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
|
|
2218
|
+
} else {
|
|
2219
|
+
await transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
|
|
2220
|
+
}
|
|
2221
|
+
} else {
|
|
2222
|
+
if (transcodeInfo.method === "remux") {
|
|
2223
|
+
await remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
|
|
2224
|
+
} else {
|
|
2225
|
+
await transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (onProgress) {
|
|
2229
|
+
onProgress({ percent: 100, duration });
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
success: true,
|
|
2233
|
+
outputPath
|
|
2234
|
+
};
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
return {
|
|
2237
|
+
success: false,
|
|
2238
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async function cleanupTranscodedFile(filePath) {
|
|
2243
|
+
try {
|
|
2244
|
+
if (filePath.includes("file-explorer-media")) {
|
|
2245
|
+
await fs13.unlink(filePath);
|
|
2246
|
+
}
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
async function cleanupAllTranscodedFiles(tempDir) {
|
|
2251
|
+
const dir = tempDir || path16.join(os3.tmpdir(), "file-explorer-media");
|
|
2252
|
+
try {
|
|
2253
|
+
const files = await fs13.readdir(dir);
|
|
2254
|
+
await Promise.all(
|
|
2255
|
+
files.map((file) => fs13.unlink(path16.join(dir, file)).catch(() => {
|
|
2256
|
+
}))
|
|
2257
|
+
);
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// src/media/service.ts
|
|
2263
|
+
import path17 from "path";
|
|
2264
|
+
import { execFile as execFile2 } from "child_process";
|
|
2265
|
+
import { promisify as promisify4 } from "util";
|
|
2266
|
+
var execFileAsync2 = promisify4(execFile2);
|
|
2267
|
+
var mediaServiceInstance = null;
|
|
2268
|
+
var MediaService = class {
|
|
2269
|
+
ffmpegPath;
|
|
2270
|
+
ffprobePath;
|
|
2271
|
+
tempDir;
|
|
2272
|
+
urlEncoder;
|
|
2273
|
+
// 缓存转码信息,避免重复检测
|
|
2274
|
+
transcodeInfoCache = /* @__PURE__ */ new Map();
|
|
2275
|
+
// 缓存已转码文件路径
|
|
2276
|
+
transcodedFiles = /* @__PURE__ */ new Map();
|
|
2277
|
+
constructor(options) {
|
|
2278
|
+
this.ffmpegPath = options.ffmpegPath;
|
|
2279
|
+
this.ffprobePath = options.ffprobePath || path17.join(path17.dirname(options.ffmpegPath), "ffprobe");
|
|
2280
|
+
this.tempDir = options.tempDir;
|
|
2281
|
+
this.urlEncoder = options.urlEncoder;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* 检测文件是否需要转码
|
|
2285
|
+
*/
|
|
2286
|
+
async needsTranscode(filePath) {
|
|
2287
|
+
const cached = this.transcodeInfoCache.get(filePath);
|
|
2288
|
+
if (cached) {
|
|
2289
|
+
return cached;
|
|
2290
|
+
}
|
|
2291
|
+
const info = await detectTranscodeNeeds(filePath, this.ffprobePath);
|
|
2292
|
+
this.transcodeInfoCache.set(filePath, info);
|
|
2293
|
+
return info;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* 执行转码并返回可播放的 URL
|
|
2297
|
+
*/
|
|
2298
|
+
async transcode(filePath, onProgress) {
|
|
2299
|
+
const existingOutput = this.transcodedFiles.get(filePath);
|
|
2300
|
+
if (existingOutput) {
|
|
2301
|
+
return {
|
|
2302
|
+
success: true,
|
|
2303
|
+
outputPath: existingOutput,
|
|
2304
|
+
url: this.urlEncoder ? this.urlEncoder(existingOutput) : `file://${existingOutput}`
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
const transcodeInfo = await this.needsTranscode(filePath);
|
|
2308
|
+
if (!transcodeInfo.needsTranscode) {
|
|
2309
|
+
const url = this.urlEncoder ? this.urlEncoder(filePath) : `file://${filePath}`;
|
|
2310
|
+
return {
|
|
2311
|
+
success: true,
|
|
2312
|
+
outputPath: filePath,
|
|
2313
|
+
url
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
const result = await transcodeMedia(
|
|
2317
|
+
this.ffmpegPath,
|
|
2318
|
+
filePath,
|
|
2319
|
+
transcodeInfo,
|
|
2320
|
+
this.tempDir,
|
|
2321
|
+
onProgress
|
|
2322
|
+
);
|
|
2323
|
+
if (result.success && result.outputPath) {
|
|
2324
|
+
this.transcodedFiles.set(filePath, result.outputPath);
|
|
2325
|
+
result.url = this.urlEncoder ? this.urlEncoder(result.outputPath) : `file://${result.outputPath}`;
|
|
2326
|
+
}
|
|
2327
|
+
return result;
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* 获取媒体元数据
|
|
2331
|
+
*/
|
|
2332
|
+
async getMetadata(filePath) {
|
|
2333
|
+
try {
|
|
2334
|
+
const { stdout } = await execFileAsync2(this.ffprobePath, [
|
|
2335
|
+
"-v",
|
|
2336
|
+
"quiet",
|
|
2337
|
+
"-print_format",
|
|
2338
|
+
"json",
|
|
2339
|
+
"-show_format",
|
|
2340
|
+
"-show_streams",
|
|
2341
|
+
filePath
|
|
2342
|
+
]);
|
|
2343
|
+
const data = JSON.parse(stdout);
|
|
2344
|
+
const format = data.format || {};
|
|
2345
|
+
const tags = format.tags || {};
|
|
2346
|
+
const formatInfo = await getMediaFormat(filePath, this.ffprobePath);
|
|
2347
|
+
if (!formatInfo) return null;
|
|
2348
|
+
return {
|
|
2349
|
+
filePath,
|
|
2350
|
+
type: formatInfo.type,
|
|
2351
|
+
duration: parseFloat(format.duration) || 0,
|
|
2352
|
+
format: formatInfo,
|
|
2353
|
+
title: tags.title || tags.TITLE,
|
|
2354
|
+
artist: tags.artist || tags.ARTIST,
|
|
2355
|
+
album: tags.album || tags.ALBUM,
|
|
2356
|
+
year: tags.date || tags.DATE || tags.year || tags.YEAR
|
|
2357
|
+
};
|
|
2358
|
+
} catch {
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* 获取可播放的 URL
|
|
2364
|
+
* 如果文件需要转码,则执行转码;否则直接返回文件 URL
|
|
2365
|
+
*/
|
|
2366
|
+
async getPlayableUrl(filePath, onProgress) {
|
|
2367
|
+
const result = await this.transcode(filePath, onProgress);
|
|
2368
|
+
return result.success ? result.url || null : null;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* 清理指定文件的转码缓存
|
|
2372
|
+
*/
|
|
2373
|
+
async cleanupFile(filePath) {
|
|
2374
|
+
const transcodedPath = this.transcodedFiles.get(filePath);
|
|
2375
|
+
if (transcodedPath) {
|
|
2376
|
+
await cleanupTranscodedFile(transcodedPath);
|
|
2377
|
+
this.transcodedFiles.delete(filePath);
|
|
2378
|
+
}
|
|
2379
|
+
this.transcodeInfoCache.delete(filePath);
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* 清理所有转码缓存
|
|
2383
|
+
*/
|
|
2384
|
+
async cleanup() {
|
|
2385
|
+
await cleanupAllTranscodedFiles(this.tempDir);
|
|
2386
|
+
this.transcodedFiles.clear();
|
|
2387
|
+
this.transcodeInfoCache.clear();
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* 清除缓存(不删除文件)
|
|
2391
|
+
*/
|
|
2392
|
+
clearCache() {
|
|
2393
|
+
this.transcodeInfoCache.clear();
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
function initMediaService(options) {
|
|
2397
|
+
mediaServiceInstance = new MediaService(options);
|
|
2398
|
+
return mediaServiceInstance;
|
|
2399
|
+
}
|
|
2400
|
+
function getMediaService() {
|
|
2401
|
+
return mediaServiceInstance;
|
|
2402
|
+
}
|
|
2403
|
+
function createMediaService(options) {
|
|
2404
|
+
return new MediaService(options);
|
|
2405
|
+
}
|
|
1173
2406
|
export {
|
|
1174
2407
|
APP_PROTOCOL_HOST,
|
|
1175
2408
|
APP_PROTOCOL_PREFIX,
|
|
1176
2409
|
APP_PROTOCOL_SCHEME,
|
|
1177
2410
|
FileType,
|
|
2411
|
+
MediaService,
|
|
1178
2412
|
SqliteThumbnailDatabase,
|
|
1179
2413
|
ThumbnailService,
|
|
2414
|
+
WatchManager,
|
|
2415
|
+
cleanupAllTranscodedFiles,
|
|
2416
|
+
cleanupTranscodedFile,
|
|
2417
|
+
closeThumbnailDatabase,
|
|
2418
|
+
compressFiles,
|
|
1180
2419
|
copyFiles,
|
|
1181
2420
|
copyFilesToClipboard,
|
|
1182
2421
|
createFfmpegVideoProcessor,
|
|
1183
2422
|
createFile,
|
|
1184
2423
|
createFolder,
|
|
2424
|
+
createMediaService,
|
|
1185
2425
|
createSharpImageProcessor,
|
|
1186
2426
|
createSqliteThumbnailDatabase,
|
|
1187
2427
|
decodeFileUrl,
|
|
1188
2428
|
deleteFiles,
|
|
2429
|
+
detectArchiveFormat,
|
|
2430
|
+
detectTranscodeNeeds,
|
|
1189
2431
|
encodeFileUrl,
|
|
1190
2432
|
exists,
|
|
2433
|
+
extractArchive,
|
|
1191
2434
|
formatDate,
|
|
1192
2435
|
formatDateTime,
|
|
1193
2436
|
formatFileSize,
|
|
@@ -1195,28 +2438,39 @@ export {
|
|
|
1195
2438
|
getApplicationIcon,
|
|
1196
2439
|
getClipboardFiles,
|
|
1197
2440
|
getFileHash,
|
|
1198
|
-
getFileHashes,
|
|
1199
2441
|
getFileInfo,
|
|
1200
2442
|
getFileType,
|
|
1201
2443
|
getHomeDirectory,
|
|
1202
|
-
|
|
2444
|
+
getMediaFormat,
|
|
2445
|
+
getMediaService,
|
|
2446
|
+
getMediaTypeByExtension,
|
|
2447
|
+
getPlatform2 as getPlatform,
|
|
1203
2448
|
getSqliteThumbnailDatabase,
|
|
1204
2449
|
getSystemPath,
|
|
1205
2450
|
getThumbnailService,
|
|
2451
|
+
getWatchManager,
|
|
2452
|
+
initMediaService,
|
|
1206
2453
|
initThumbnailService,
|
|
1207
2454
|
isAppProtocolUrl,
|
|
2455
|
+
isArchiveFile,
|
|
1208
2456
|
isDirectory,
|
|
1209
2457
|
isMediaFile,
|
|
1210
2458
|
isPreviewable,
|
|
1211
2459
|
moveFiles,
|
|
2460
|
+
openInEditor,
|
|
2461
|
+
openInTerminal,
|
|
1212
2462
|
pasteFiles,
|
|
1213
2463
|
readDirectory,
|
|
1214
2464
|
readFileContent,
|
|
1215
2465
|
readImageAsBase64,
|
|
1216
2466
|
renameFile,
|
|
2467
|
+
revealInFileManager,
|
|
1217
2468
|
searchFiles,
|
|
1218
2469
|
searchFilesStream,
|
|
1219
2470
|
searchFilesSync,
|
|
2471
|
+
showFileInfo,
|
|
2472
|
+
transcodeMedia,
|
|
2473
|
+
watchDirectory,
|
|
1220
2474
|
writeFileContent
|
|
1221
2475
|
};
|
|
1222
2476
|
//# sourceMappingURL=index.js.map
|