@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 CHANGED
@@ -34,19 +34,29 @@ __export(index_exports, {
34
34
  APP_PROTOCOL_PREFIX: () => APP_PROTOCOL_PREFIX,
35
35
  APP_PROTOCOL_SCHEME: () => APP_PROTOCOL_SCHEME,
36
36
  FileType: () => FileType,
37
+ MediaService: () => MediaService,
37
38
  SqliteThumbnailDatabase: () => SqliteThumbnailDatabase,
38
39
  ThumbnailService: () => ThumbnailService,
40
+ WatchManager: () => WatchManager,
41
+ cleanupAllTranscodedFiles: () => cleanupAllTranscodedFiles,
42
+ cleanupTranscodedFile: () => cleanupTranscodedFile,
43
+ closeThumbnailDatabase: () => closeThumbnailDatabase,
44
+ compressFiles: () => compressFiles,
39
45
  copyFiles: () => copyFiles,
40
46
  copyFilesToClipboard: () => copyFilesToClipboard,
41
47
  createFfmpegVideoProcessor: () => createFfmpegVideoProcessor,
42
48
  createFile: () => createFile,
43
49
  createFolder: () => createFolder,
50
+ createMediaService: () => createMediaService,
44
51
  createSharpImageProcessor: () => createSharpImageProcessor,
45
52
  createSqliteThumbnailDatabase: () => createSqliteThumbnailDatabase,
46
53
  decodeFileUrl: () => decodeFileUrl,
47
54
  deleteFiles: () => deleteFiles,
55
+ detectArchiveFormat: () => detectArchiveFormat,
56
+ detectTranscodeNeeds: () => detectTranscodeNeeds,
48
57
  encodeFileUrl: () => encodeFileUrl,
49
58
  exists: () => exists,
59
+ extractArchive: () => extractArchive,
50
60
  formatDate: () => formatDate,
51
61
  formatDateTime: () => formatDateTime,
52
62
  formatFileSize: () => formatFileSize,
@@ -54,28 +64,39 @@ __export(index_exports, {
54
64
  getApplicationIcon: () => getApplicationIcon,
55
65
  getClipboardFiles: () => getClipboardFiles,
56
66
  getFileHash: () => getFileHash,
57
- getFileHashes: () => getFileHashes,
58
67
  getFileInfo: () => getFileInfo,
59
68
  getFileType: () => getFileType,
60
69
  getHomeDirectory: () => getHomeDirectory,
61
- getPlatform: () => getPlatform,
70
+ getMediaFormat: () => getMediaFormat,
71
+ getMediaService: () => getMediaService,
72
+ getMediaTypeByExtension: () => getMediaTypeByExtension,
73
+ getPlatform: () => getPlatform2,
62
74
  getSqliteThumbnailDatabase: () => getSqliteThumbnailDatabase,
63
75
  getSystemPath: () => getSystemPath,
64
76
  getThumbnailService: () => getThumbnailService,
77
+ getWatchManager: () => getWatchManager,
78
+ initMediaService: () => initMediaService,
65
79
  initThumbnailService: () => initThumbnailService,
66
80
  isAppProtocolUrl: () => isAppProtocolUrl,
81
+ isArchiveFile: () => isArchiveFile,
67
82
  isDirectory: () => isDirectory,
68
83
  isMediaFile: () => isMediaFile,
69
84
  isPreviewable: () => isPreviewable,
70
85
  moveFiles: () => moveFiles,
86
+ openInEditor: () => openInEditor,
87
+ openInTerminal: () => openInTerminal,
71
88
  pasteFiles: () => pasteFiles,
72
89
  readDirectory: () => readDirectory,
73
90
  readFileContent: () => readFileContent,
74
91
  readImageAsBase64: () => readImageAsBase64,
75
92
  renameFile: () => renameFile,
93
+ revealInFileManager: () => revealInFileManager,
76
94
  searchFiles: () => searchFiles,
77
95
  searchFilesStream: () => searchFilesStream,
78
96
  searchFilesSync: () => searchFilesSync,
97
+ showFileInfo: () => showFileInfo,
98
+ transcodeMedia: () => transcodeMedia,
99
+ watchDirectory: () => watchDirectory,
79
100
  writeFileContent: () => writeFileContent
80
101
  });
81
102
  module.exports = __toCommonJS(index_exports);
@@ -107,22 +128,22 @@ function encodeFileUrl(filePath) {
107
128
  }
108
129
  function decodeFileUrl(url) {
109
130
  if (!url) return "";
110
- let path12 = url;
111
- if (path12.startsWith(APP_PROTOCOL_PREFIX)) {
112
- path12 = path12.slice(APP_PROTOCOL_PREFIX.length);
131
+ let path18 = url;
132
+ if (path18.startsWith(APP_PROTOCOL_PREFIX)) {
133
+ path18 = path18.slice(APP_PROTOCOL_PREFIX.length);
113
134
  }
114
- const hashIndex = path12.indexOf("#");
135
+ const hashIndex = path18.indexOf("#");
115
136
  if (hashIndex !== -1) {
116
- path12 = path12.substring(0, hashIndex);
137
+ path18 = path18.substring(0, hashIndex);
117
138
  }
118
- const queryIndex = path12.indexOf("?");
139
+ const queryIndex = path18.indexOf("?");
119
140
  if (queryIndex !== -1) {
120
- path12 = path12.substring(0, queryIndex);
141
+ path18 = path18.substring(0, queryIndex);
121
142
  }
122
143
  try {
123
- return decodeURIComponent(path12);
144
+ return decodeURIComponent(path18);
124
145
  } catch {
125
- return path12;
146
+ return path18;
126
147
  }
127
148
  }
128
149
  function isAppProtocolUrl(url) {
@@ -470,16 +491,16 @@ async function createFile(filePath, content = "") {
470
491
  await import_node_fs2.promises.writeFile(filePath, content, "utf-8");
471
492
  return { success: true, data: { finalPath: filePath } };
472
493
  }
473
- const dirname = import_node_path3.default.dirname(filePath);
494
+ const dirname3 = import_node_path3.default.dirname(filePath);
474
495
  const ext = import_node_path3.default.extname(filePath);
475
- const basename = import_node_path3.default.basename(filePath, ext);
496
+ const basename2 = import_node_path3.default.basename(filePath, ext);
476
497
  let counter = 2;
477
- let finalPath = import_node_path3.default.join(dirname, `${basename} ${counter}${ext}`);
498
+ let finalPath = import_node_path3.default.join(dirname3, `${basename2} ${counter}${ext}`);
478
499
  while (true) {
479
500
  try {
480
501
  await import_node_fs2.promises.access(finalPath);
481
502
  counter++;
482
- finalPath = import_node_path3.default.join(dirname, `${basename} ${counter}${ext}`);
503
+ finalPath = import_node_path3.default.join(dirname3, `${basename2} ${counter}${ext}`);
483
504
  } catch {
484
505
  break;
485
506
  }
@@ -655,6 +676,468 @@ async function isDirectory(filePath) {
655
676
  }
656
677
  }
657
678
 
679
+ // src/operations/shell.ts
680
+ var import_node_child_process = require("child_process");
681
+ var import_node_util = require("util");
682
+ var path6 = __toESM(require("path"), 1);
683
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
684
+ function getPlatform() {
685
+ switch (process.platform) {
686
+ case "darwin":
687
+ return "mac";
688
+ case "win32":
689
+ return "windows";
690
+ default:
691
+ return "linux";
692
+ }
693
+ }
694
+ async function showFileInfo(filePath) {
695
+ const platform2 = getPlatform();
696
+ try {
697
+ switch (platform2) {
698
+ case "mac": {
699
+ const script = `tell application "Finder"
700
+ activate
701
+ set theFile to POSIX file "${filePath}" as alias
702
+ open information window of theFile
703
+ end tell`;
704
+ await execAsync(`osascript -e '${script}'`);
705
+ break;
706
+ }
707
+ case "windows": {
708
+ const escapedPath = filePath.replace(/'/g, "''");
709
+ const psScript = `
710
+ $shell = New-Object -ComObject Shell.Application
711
+ $folder = $shell.Namespace((Split-Path '${escapedPath}'))
712
+ $item = $folder.ParseName((Split-Path '${escapedPath}' -Leaf))
713
+ $item.InvokeVerb('properties')
714
+ `;
715
+ await execAsync(`powershell -Command "${psScript.replace(/\n/g, " ")}"`);
716
+ break;
717
+ }
718
+ case "linux": {
719
+ const commands = [
720
+ `nautilus --select "${filePath}" && nautilus -q`,
721
+ // GNOME
722
+ `dolphin --select "${filePath}"`,
723
+ // KDE
724
+ `thunar --quit && thunar "${path6.dirname(filePath)}"`,
725
+ // XFCE
726
+ `xdg-open "${path6.dirname(filePath)}"`
727
+ // 通用
728
+ ];
729
+ let lastError = null;
730
+ for (const cmd of commands) {
731
+ try {
732
+ await execAsync(cmd);
733
+ return { success: true };
734
+ } catch (e) {
735
+ lastError = e;
736
+ }
737
+ }
738
+ throw lastError || new Error("No file manager found");
739
+ }
740
+ }
741
+ return { success: true };
742
+ } catch (error) {
743
+ return {
744
+ success: false,
745
+ error: error instanceof Error ? error.message : String(error)
746
+ };
747
+ }
748
+ }
749
+ async function revealInFileManager(filePath) {
750
+ const platform2 = getPlatform();
751
+ try {
752
+ switch (platform2) {
753
+ case "mac": {
754
+ await execAsync(`open -R "${filePath}"`);
755
+ break;
756
+ }
757
+ case "windows": {
758
+ await execAsync(`explorer.exe /select,"${filePath}"`);
759
+ break;
760
+ }
761
+ case "linux": {
762
+ await execAsync(`xdg-open "${path6.dirname(filePath)}"`);
763
+ break;
764
+ }
765
+ }
766
+ return { success: true };
767
+ } catch (error) {
768
+ return {
769
+ success: false,
770
+ error: error instanceof Error ? error.message : String(error)
771
+ };
772
+ }
773
+ }
774
+ async function openInTerminal(dirPath) {
775
+ const platform2 = getPlatform();
776
+ try {
777
+ switch (platform2) {
778
+ case "mac": {
779
+ try {
780
+ const itermScript = `tell application "iTerm"
781
+ activate
782
+ try
783
+ set newWindow to (create window with default profile)
784
+ tell current session of newWindow
785
+ write text "cd '${dirPath}'"
786
+ end tell
787
+ on error
788
+ tell current window
789
+ create tab with default profile
790
+ tell current session
791
+ write text "cd '${dirPath}'"
792
+ end tell
793
+ end tell
794
+ end try
795
+ end tell`;
796
+ await execAsync(`osascript -e '${itermScript}'`);
797
+ } catch {
798
+ const terminalScript = `tell application "Terminal"
799
+ activate
800
+ do script "cd '${dirPath}'"
801
+ end tell`;
802
+ await execAsync(`osascript -e '${terminalScript}'`);
803
+ }
804
+ break;
805
+ }
806
+ case "windows": {
807
+ try {
808
+ await execAsync(`wt -d "${dirPath}"`);
809
+ } catch {
810
+ await execAsync(`start cmd /K "cd /d ${dirPath}"`);
811
+ }
812
+ break;
813
+ }
814
+ case "linux": {
815
+ const terminals = [
816
+ `gnome-terminal --working-directory="${dirPath}"`,
817
+ `konsole --workdir "${dirPath}"`,
818
+ `xfce4-terminal --working-directory="${dirPath}"`,
819
+ `xterm -e "cd '${dirPath}' && $SHELL"`
820
+ ];
821
+ let lastError = null;
822
+ for (const cmd of terminals) {
823
+ try {
824
+ await execAsync(cmd);
825
+ return { success: true };
826
+ } catch (e) {
827
+ lastError = e;
828
+ }
829
+ }
830
+ throw lastError || new Error("No terminal found");
831
+ }
832
+ }
833
+ return { success: true };
834
+ } catch (error) {
835
+ return {
836
+ success: false,
837
+ error: error instanceof Error ? error.message : String(error)
838
+ };
839
+ }
840
+ }
841
+ async function openInEditor(targetPath) {
842
+ const platform2 = getPlatform();
843
+ try {
844
+ const editors = platform2 === "mac" ? [
845
+ // macOS: 优先 Cursor,回退到 VSCode
846
+ `open -a "Cursor" "${targetPath}"`,
847
+ `open -a "Visual Studio Code" "${targetPath}"`,
848
+ `code "${targetPath}"`
849
+ ] : platform2 === "windows" ? [
850
+ // Windows
851
+ `cursor "${targetPath}"`,
852
+ `code "${targetPath}"`
853
+ ] : [
854
+ // Linux
855
+ `cursor "${targetPath}"`,
856
+ `code "${targetPath}"`
857
+ ];
858
+ let lastError = null;
859
+ for (const cmd of editors) {
860
+ try {
861
+ await execAsync(cmd);
862
+ return { success: true };
863
+ } catch (e) {
864
+ lastError = e;
865
+ }
866
+ }
867
+ throw lastError || new Error("No editor found");
868
+ } catch (error) {
869
+ return {
870
+ success: false,
871
+ error: error instanceof Error ? error.message : String(error)
872
+ };
873
+ }
874
+ }
875
+
876
+ // src/operations/compress.ts
877
+ var compressing = __toESM(require("compressing"), 1);
878
+ var path7 = __toESM(require("path"), 1);
879
+ var fs7 = __toESM(require("fs"), 1);
880
+ var import_node_fs7 = require("fs");
881
+ var import_promises = require("stream/promises");
882
+ function getExtension(format) {
883
+ switch (format) {
884
+ case "zip":
885
+ return ".zip";
886
+ case "tar":
887
+ return ".tar";
888
+ case "tgz":
889
+ return ".tar.gz";
890
+ case "tarbz2":
891
+ return ".tar.bz2";
892
+ default:
893
+ return ".zip";
894
+ }
895
+ }
896
+ function detectArchiveFormat(filePath) {
897
+ const lowerPath = filePath.toLowerCase();
898
+ if (lowerPath.endsWith(".zip")) return "zip";
899
+ if (lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz")) return "tgz";
900
+ if (lowerPath.endsWith(".tar.bz2") || lowerPath.endsWith(".tbz2")) return "tarbz2";
901
+ if (lowerPath.endsWith(".tar")) return "tar";
902
+ return null;
903
+ }
904
+ function isArchiveFile(filePath) {
905
+ return detectArchiveFormat(filePath) !== null;
906
+ }
907
+ async function getAllFiles(dirPath) {
908
+ const files = [];
909
+ const entries = await import_node_fs7.promises.readdir(dirPath, { withFileTypes: true });
910
+ for (const entry of entries) {
911
+ const fullPath = path7.join(dirPath, entry.name);
912
+ if (entry.isDirectory()) {
913
+ files.push(...await getAllFiles(fullPath));
914
+ } else {
915
+ files.push(fullPath);
916
+ }
917
+ }
918
+ return files;
919
+ }
920
+ async function countFiles(sources) {
921
+ let count = 0;
922
+ for (const source of sources) {
923
+ const stats = await import_node_fs7.promises.stat(source);
924
+ if (stats.isDirectory()) {
925
+ const files = await getAllFiles(source);
926
+ count += files.length;
927
+ } else {
928
+ count += 1;
929
+ }
930
+ }
931
+ return count;
932
+ }
933
+ async function compressFiles(sources, options, onProgress) {
934
+ try {
935
+ const { format, level = "normal", outputName, outputDir, deleteSource } = options;
936
+ const ext = getExtension(format);
937
+ const finalName = outputName.endsWith(ext) ? outputName : outputName + ext;
938
+ const outputPath = path7.join(outputDir, finalName);
939
+ await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
940
+ const totalCount = await countFiles(sources);
941
+ let processedCount = 0;
942
+ switch (format) {
943
+ case "zip": {
944
+ const stream = new compressing.zip.Stream();
945
+ for (const source of sources) {
946
+ const stats = await import_node_fs7.promises.stat(source);
947
+ const baseName = path7.basename(source);
948
+ if (stats.isDirectory()) {
949
+ const files = await getAllFiles(source);
950
+ for (const file of files) {
951
+ const relativePath = path7.relative(path7.dirname(source), file);
952
+ stream.addEntry(file, { relativePath });
953
+ processedCount++;
954
+ onProgress?.({
955
+ currentFile: path7.basename(file),
956
+ processedCount,
957
+ totalCount,
958
+ percent: Math.round(processedCount / totalCount * 100)
959
+ });
960
+ }
961
+ } else {
962
+ stream.addEntry(source, { relativePath: baseName });
963
+ processedCount++;
964
+ onProgress?.({
965
+ currentFile: baseName,
966
+ processedCount,
967
+ totalCount,
968
+ percent: Math.round(processedCount / totalCount * 100)
969
+ });
970
+ }
971
+ }
972
+ const destStream = fs7.createWriteStream(outputPath);
973
+ await (0, import_promises.pipeline)(stream, destStream);
974
+ break;
975
+ }
976
+ case "tar": {
977
+ const stream = new compressing.tar.Stream();
978
+ for (const source of sources) {
979
+ const stats = await import_node_fs7.promises.stat(source);
980
+ const baseName = path7.basename(source);
981
+ if (stats.isDirectory()) {
982
+ const files = await getAllFiles(source);
983
+ for (const file of files) {
984
+ const relativePath = path7.relative(path7.dirname(source), file);
985
+ stream.addEntry(file, { relativePath });
986
+ processedCount++;
987
+ onProgress?.({
988
+ currentFile: path7.basename(file),
989
+ processedCount,
990
+ totalCount,
991
+ percent: Math.round(processedCount / totalCount * 100)
992
+ });
993
+ }
994
+ } else {
995
+ stream.addEntry(source, { relativePath: baseName });
996
+ processedCount++;
997
+ onProgress?.({
998
+ currentFile: baseName,
999
+ processedCount,
1000
+ totalCount,
1001
+ percent: Math.round(processedCount / totalCount * 100)
1002
+ });
1003
+ }
1004
+ }
1005
+ const destStream = fs7.createWriteStream(outputPath);
1006
+ await (0, import_promises.pipeline)(stream, destStream);
1007
+ break;
1008
+ }
1009
+ case "tgz": {
1010
+ const stream = new compressing.tgz.Stream();
1011
+ for (const source of sources) {
1012
+ const stats = await import_node_fs7.promises.stat(source);
1013
+ const baseName = path7.basename(source);
1014
+ if (stats.isDirectory()) {
1015
+ const files = await getAllFiles(source);
1016
+ for (const file of files) {
1017
+ const relativePath = path7.relative(path7.dirname(source), file);
1018
+ stream.addEntry(file, { relativePath });
1019
+ processedCount++;
1020
+ onProgress?.({
1021
+ currentFile: path7.basename(file),
1022
+ processedCount,
1023
+ totalCount,
1024
+ percent: Math.round(processedCount / totalCount * 100)
1025
+ });
1026
+ }
1027
+ } else {
1028
+ stream.addEntry(source, { relativePath: baseName });
1029
+ processedCount++;
1030
+ onProgress?.({
1031
+ currentFile: baseName,
1032
+ processedCount,
1033
+ totalCount,
1034
+ percent: Math.round(processedCount / totalCount * 100)
1035
+ });
1036
+ }
1037
+ }
1038
+ const destStream = fs7.createWriteStream(outputPath);
1039
+ await (0, import_promises.pipeline)(stream, destStream);
1040
+ break;
1041
+ }
1042
+ case "tarbz2": {
1043
+ console.warn("tar.bz2 format not fully supported, using tgz instead");
1044
+ const tgzStream = new compressing.tgz.Stream();
1045
+ for (const source of sources) {
1046
+ const stats = await import_node_fs7.promises.stat(source);
1047
+ const baseName = path7.basename(source);
1048
+ if (stats.isDirectory()) {
1049
+ const files = await getAllFiles(source);
1050
+ for (const file of files) {
1051
+ const relativePath = path7.relative(path7.dirname(source), file);
1052
+ tgzStream.addEntry(file, { relativePath });
1053
+ processedCount++;
1054
+ onProgress?.({
1055
+ currentFile: path7.basename(file),
1056
+ processedCount,
1057
+ totalCount,
1058
+ percent: Math.round(processedCount / totalCount * 100)
1059
+ });
1060
+ }
1061
+ } else {
1062
+ tgzStream.addEntry(source, { relativePath: baseName });
1063
+ processedCount++;
1064
+ onProgress?.({
1065
+ currentFile: baseName,
1066
+ processedCount,
1067
+ totalCount,
1068
+ percent: Math.round(processedCount / totalCount * 100)
1069
+ });
1070
+ }
1071
+ }
1072
+ const destStream = fs7.createWriteStream(outputPath.replace(".tar.bz2", ".tar.gz"));
1073
+ await (0, import_promises.pipeline)(tgzStream, destStream);
1074
+ break;
1075
+ }
1076
+ }
1077
+ if (deleteSource) {
1078
+ for (const source of sources) {
1079
+ const stats = await import_node_fs7.promises.stat(source);
1080
+ if (stats.isDirectory()) {
1081
+ await import_node_fs7.promises.rm(source, { recursive: true });
1082
+ } else {
1083
+ await import_node_fs7.promises.unlink(source);
1084
+ }
1085
+ }
1086
+ }
1087
+ return { success: true, outputPath };
1088
+ } catch (error) {
1089
+ return {
1090
+ success: false,
1091
+ error: error instanceof Error ? error.message : String(error)
1092
+ };
1093
+ }
1094
+ }
1095
+ async function extractArchive(archivePath, options, onProgress) {
1096
+ try {
1097
+ const { targetDir, deleteArchive } = options;
1098
+ const format = detectArchiveFormat(archivePath);
1099
+ if (!format) {
1100
+ return { success: false, error: "\u4E0D\u652F\u6301\u7684\u538B\u7F29\u683C\u5F0F" };
1101
+ }
1102
+ await import_node_fs7.promises.mkdir(targetDir, { recursive: true });
1103
+ onProgress?.({
1104
+ currentFile: path7.basename(archivePath),
1105
+ processedCount: 0,
1106
+ totalCount: 1,
1107
+ percent: 0
1108
+ });
1109
+ switch (format) {
1110
+ case "zip":
1111
+ await compressing.zip.uncompress(archivePath, targetDir);
1112
+ break;
1113
+ case "tar":
1114
+ await compressing.tar.uncompress(archivePath, targetDir);
1115
+ break;
1116
+ case "tgz":
1117
+ await compressing.tgz.uncompress(archivePath, targetDir);
1118
+ break;
1119
+ case "tarbz2":
1120
+ console.warn("tar.bz2 format not fully supported");
1121
+ return { success: false, error: "tar.bz2 \u683C\u5F0F\u6682\u4E0D\u652F\u6301" };
1122
+ }
1123
+ onProgress?.({
1124
+ currentFile: path7.basename(archivePath),
1125
+ processedCount: 1,
1126
+ totalCount: 1,
1127
+ percent: 100
1128
+ });
1129
+ if (deleteArchive) {
1130
+ await import_node_fs7.promises.unlink(archivePath);
1131
+ }
1132
+ return { success: true, outputPath: targetDir };
1133
+ } catch (error) {
1134
+ return {
1135
+ success: false,
1136
+ error: error instanceof Error ? error.message : String(error)
1137
+ };
1138
+ }
1139
+ }
1140
+
658
1141
  // src/system-paths.ts
659
1142
  var import_node_path6 = __toESM(require("path"), 1);
660
1143
  var import_node_os = __toESM(require("os"), 1);
@@ -705,14 +1188,14 @@ function getAllSystemPaths() {
705
1188
  function getHomeDirectory() {
706
1189
  return import_node_os.default.homedir();
707
1190
  }
708
- function getPlatform() {
1191
+ function getPlatform2() {
709
1192
  return process.platform;
710
1193
  }
711
1194
 
712
1195
  // src/search.ts
713
1196
  var import_fdir = require("fdir");
714
1197
  var import_node_path7 = __toESM(require("path"), 1);
715
- var import_node_fs7 = require("fs");
1198
+ var import_node_fs8 = require("fs");
716
1199
  function patternToRegex(pattern) {
717
1200
  return new RegExp(pattern.replace(/\*/g, ".*"), "i");
718
1201
  }
@@ -736,7 +1219,7 @@ async function searchFilesStream(searchPath, pattern, onResults, maxResults = 10
736
1219
  async function searchDir(dirPath) {
737
1220
  if (stopped) return;
738
1221
  try {
739
- const entries = await import_node_fs7.promises.readdir(dirPath, { withFileTypes: true });
1222
+ const entries = await import_node_fs8.promises.readdir(dirPath, { withFileTypes: true });
740
1223
  const matched = [];
741
1224
  const subdirs = [];
742
1225
  for (const entry of entries) {
@@ -784,38 +1267,23 @@ function searchFilesSync(searchPath, pattern, maxDepth) {
784
1267
  }
785
1268
 
786
1269
  // src/hash.ts
787
- var import_node_crypto = require("crypto");
788
- var import_promises = require("fs/promises");
789
- async function getFileHash(filePath) {
790
- try {
791
- const stats = await (0, import_promises.stat)(filePath);
792
- const hashInput = `${filePath}:${stats.size}:${stats.mtime.getTime()}`;
793
- return (0, import_node_crypto.createHash)("md5").update(hashInput).digest("hex");
794
- } catch (error) {
795
- console.error(`Error computing hash for ${filePath}:`, error);
796
- return (0, import_node_crypto.createHash)("md5").update(filePath).digest("hex");
797
- }
798
- }
799
- async function getFileHashes(filePaths) {
800
- const hashMap = /* @__PURE__ */ new Map();
801
- await Promise.allSettled(
802
- filePaths.map(async (filePath) => {
803
- const hash = await getFileHash(filePath);
804
- hashMap.set(filePath, hash);
805
- })
806
- );
807
- return hashMap;
1270
+ var import_hash_wasm = require("hash-wasm");
1271
+ var import_promises2 = require("fs/promises");
1272
+ async function getFileHash(filePath, stats) {
1273
+ const fileStats = stats || await (0, import_promises2.stat)(filePath);
1274
+ const hashInput = `${filePath}:${fileStats.size}:${fileStats.mtime.getTime()}`;
1275
+ return await (0, import_hash_wasm.xxhash64)(hashInput);
808
1276
  }
809
1277
 
810
1278
  // src/clipboard.ts
811
- var import_node_fs8 = require("fs");
1279
+ var import_node_fs9 = require("fs");
812
1280
  var import_node_path8 = __toESM(require("path"), 1);
813
1281
  async function copyFilesToClipboard(filePaths, clipboard) {
814
1282
  try {
815
1283
  const cleanPaths = [];
816
1284
  for (const p of filePaths) {
817
1285
  try {
818
- await import_node_fs8.promises.access(p);
1286
+ await import_node_fs9.promises.access(p);
819
1287
  cleanPaths.push(p);
820
1288
  } catch {
821
1289
  }
@@ -855,7 +1323,7 @@ async function pasteFiles(targetDir, sourcePaths) {
855
1323
  let counter = 1;
856
1324
  while (true) {
857
1325
  try {
858
- await import_node_fs8.promises.access(destPath);
1326
+ await import_node_fs9.promises.access(destPath);
859
1327
  const ext = import_node_path8.default.extname(fileName);
860
1328
  const baseName = import_node_path8.default.basename(fileName, ext);
861
1329
  destPath = import_node_path8.default.join(targetDir, `${baseName} ${++counter}${ext}`);
@@ -863,11 +1331,11 @@ async function pasteFiles(targetDir, sourcePaths) {
863
1331
  break;
864
1332
  }
865
1333
  }
866
- const stats = await import_node_fs8.promises.stat(sourcePath);
1334
+ const stats = await import_node_fs9.promises.stat(sourcePath);
867
1335
  if (stats.isDirectory()) {
868
1336
  await copyDirectory2(sourcePath, destPath);
869
1337
  } else {
870
- await import_node_fs8.promises.copyFile(sourcePath, destPath);
1338
+ await import_node_fs9.promises.copyFile(sourcePath, destPath);
871
1339
  }
872
1340
  pastedPaths.push(destPath);
873
1341
  }
@@ -877,26 +1345,26 @@ async function pasteFiles(targetDir, sourcePaths) {
877
1345
  }
878
1346
  }
879
1347
  async function copyDirectory2(source, dest) {
880
- await import_node_fs8.promises.mkdir(dest, { recursive: true });
881
- const entries = await import_node_fs8.promises.readdir(source, { withFileTypes: true });
1348
+ await import_node_fs9.promises.mkdir(dest, { recursive: true });
1349
+ const entries = await import_node_fs9.promises.readdir(source, { withFileTypes: true });
882
1350
  for (const entry of entries) {
883
1351
  const sourcePath = import_node_path8.default.join(source, entry.name);
884
1352
  const destPath = import_node_path8.default.join(dest, entry.name);
885
1353
  if (entry.isDirectory()) {
886
1354
  await copyDirectory2(sourcePath, destPath);
887
1355
  } else {
888
- await import_node_fs8.promises.copyFile(sourcePath, destPath);
1356
+ await import_node_fs9.promises.copyFile(sourcePath, destPath);
889
1357
  }
890
1358
  }
891
1359
  }
892
1360
 
893
1361
  // src/application-icon.ts
894
- var import_node_fs9 = require("fs");
1362
+ var import_node_fs10 = require("fs");
895
1363
  var import_node_path9 = __toESM(require("path"), 1);
896
1364
  var import_node_os2 = __toESM(require("os"), 1);
897
- var import_node_child_process = require("child_process");
898
- var import_node_util = require("util");
899
- var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
1365
+ var import_node_child_process2 = require("child_process");
1366
+ var import_node_util2 = require("util");
1367
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
900
1368
  async function findAppIconPath(appPath) {
901
1369
  const resourcesPath = import_node_path9.default.join(appPath, "Contents", "Resources");
902
1370
  try {
@@ -908,7 +1376,7 @@ async function findAppIconPath(appPath) {
908
1376
  ];
909
1377
  const infoPlistPath = import_node_path9.default.join(appPath, "Contents", "Info.plist");
910
1378
  try {
911
- const infoPlistContent = await import_node_fs9.promises.readFile(infoPlistPath, "utf-8");
1379
+ const infoPlistContent = await import_node_fs10.promises.readFile(infoPlistPath, "utf-8");
912
1380
  const iconFileMatch = infoPlistContent.match(/<key>CFBundleIconFile<\/key>\s*<string>([^<]+)<\/string>/);
913
1381
  if (iconFileMatch && iconFileMatch[1]) {
914
1382
  let iconFileName = iconFileMatch[1].trim();
@@ -917,7 +1385,7 @@ async function findAppIconPath(appPath) {
917
1385
  }
918
1386
  const iconPath = import_node_path9.default.join(resourcesPath, iconFileName);
919
1387
  try {
920
- await import_node_fs9.promises.access(iconPath);
1388
+ await import_node_fs10.promises.access(iconPath);
921
1389
  return iconPath;
922
1390
  } catch {
923
1391
  }
@@ -927,14 +1395,14 @@ async function findAppIconPath(appPath) {
927
1395
  for (const iconName of commonIconNames) {
928
1396
  const iconPath = import_node_path9.default.join(resourcesPath, iconName);
929
1397
  try {
930
- await import_node_fs9.promises.access(iconPath);
1398
+ await import_node_fs10.promises.access(iconPath);
931
1399
  return iconPath;
932
1400
  } catch {
933
1401
  continue;
934
1402
  }
935
1403
  }
936
1404
  try {
937
- const entries = await import_node_fs9.promises.readdir(resourcesPath);
1405
+ const entries = await import_node_fs10.promises.readdir(resourcesPath);
938
1406
  const icnsFile = entries.find((entry) => entry.toLowerCase().endsWith(".icns"));
939
1407
  if (icnsFile) {
940
1408
  return import_node_path9.default.join(resourcesPath, icnsFile);
@@ -951,7 +1419,7 @@ async function getApplicationIcon(appPath) {
951
1419
  return null;
952
1420
  }
953
1421
  try {
954
- const stats = await import_node_fs9.promises.stat(appPath);
1422
+ const stats = await import_node_fs10.promises.stat(appPath);
955
1423
  if (!stats.isDirectory() || !appPath.endsWith(".app")) {
956
1424
  return null;
957
1425
  }
@@ -964,12 +1432,12 @@ async function getApplicationIcon(appPath) {
964
1432
  }
965
1433
  try {
966
1434
  const tempPngPath = import_node_path9.default.join(import_node_os2.default.tmpdir(), `app-icon-${Date.now()}.png`);
967
- await execAsync(
1435
+ await execAsync2(
968
1436
  `sips -s format png "${iconPath}" --out "${tempPngPath}" --resampleHeightWidthMax 128`
969
1437
  );
970
- const pngBuffer = await import_node_fs9.promises.readFile(tempPngPath);
1438
+ const pngBuffer = await import_node_fs10.promises.readFile(tempPngPath);
971
1439
  try {
972
- await import_node_fs9.promises.unlink(tempPngPath);
1440
+ await import_node_fs10.promises.unlink(tempPngPath);
973
1441
  } catch {
974
1442
  }
975
1443
  const base64 = pngBuffer.toString("base64");
@@ -979,11 +1447,150 @@ async function getApplicationIcon(appPath) {
979
1447
  }
980
1448
  }
981
1449
 
1450
+ // src/watch.ts
1451
+ var fs11 = __toESM(require("fs"), 1);
1452
+ var path12 = __toESM(require("path"), 1);
1453
+ var debounceTimers = /* @__PURE__ */ new Map();
1454
+ var DEBOUNCE_DELAY = 100;
1455
+ function watchDirectory(dirPath, callback) {
1456
+ let watcher = null;
1457
+ try {
1458
+ watcher = fs11.watch(dirPath, { persistent: true }, (eventType, filename) => {
1459
+ if (!filename) return;
1460
+ const fullPath = path12.join(dirPath, filename);
1461
+ const key = `${dirPath}:${filename}`;
1462
+ const existingTimer = debounceTimers.get(key);
1463
+ if (existingTimer) {
1464
+ clearTimeout(existingTimer);
1465
+ }
1466
+ const timer = setTimeout(() => {
1467
+ debounceTimers.delete(key);
1468
+ fs11.access(fullPath, fs11.constants.F_OK, (err) => {
1469
+ let type;
1470
+ if (err) {
1471
+ type = "remove";
1472
+ } else if (eventType === "rename") {
1473
+ type = "add";
1474
+ } else {
1475
+ type = "change";
1476
+ }
1477
+ callback({
1478
+ type,
1479
+ path: fullPath,
1480
+ filename
1481
+ });
1482
+ });
1483
+ }, DEBOUNCE_DELAY);
1484
+ debounceTimers.set(key, timer);
1485
+ });
1486
+ watcher.on("error", (error) => {
1487
+ console.error("Watch error:", error);
1488
+ });
1489
+ } catch (error) {
1490
+ console.error("Failed to watch directory:", error);
1491
+ }
1492
+ return {
1493
+ close: () => {
1494
+ if (watcher) {
1495
+ watcher.close();
1496
+ watcher = null;
1497
+ }
1498
+ for (const [key, timer] of debounceTimers.entries()) {
1499
+ if (key.startsWith(`${dirPath}:`)) {
1500
+ clearTimeout(timer);
1501
+ debounceTimers.delete(key);
1502
+ }
1503
+ }
1504
+ },
1505
+ path: dirPath
1506
+ };
1507
+ }
1508
+ var WatchManager = class {
1509
+ watchers = /* @__PURE__ */ new Map();
1510
+ callbacks = /* @__PURE__ */ new Map();
1511
+ /**
1512
+ * 开始监听目录
1513
+ */
1514
+ watch(dirPath, callback) {
1515
+ const normalizedPath = path12.normalize(dirPath);
1516
+ let callbackSet = this.callbacks.get(normalizedPath);
1517
+ if (!callbackSet) {
1518
+ callbackSet = /* @__PURE__ */ new Set();
1519
+ this.callbacks.set(normalizedPath, callbackSet);
1520
+ }
1521
+ callbackSet.add(callback);
1522
+ let watcherInfo = this.watchers.get(normalizedPath);
1523
+ if (watcherInfo) {
1524
+ watcherInfo.refCount++;
1525
+ } else {
1526
+ const watcher = watchDirectory(normalizedPath, (event) => {
1527
+ const callbacks = this.callbacks.get(normalizedPath);
1528
+ if (callbacks) {
1529
+ for (const cb of callbacks) {
1530
+ try {
1531
+ cb(event);
1532
+ } catch (error) {
1533
+ console.error("Watch callback error:", error);
1534
+ }
1535
+ }
1536
+ }
1537
+ });
1538
+ watcherInfo = { watcher, refCount: 1 };
1539
+ this.watchers.set(normalizedPath, watcherInfo);
1540
+ }
1541
+ return () => {
1542
+ this.unwatch(normalizedPath, callback);
1543
+ };
1544
+ }
1545
+ /**
1546
+ * 停止监听
1547
+ */
1548
+ unwatch(dirPath, callback) {
1549
+ const normalizedPath = path12.normalize(dirPath);
1550
+ const callbackSet = this.callbacks.get(normalizedPath);
1551
+ if (callbackSet) {
1552
+ callbackSet.delete(callback);
1553
+ if (callbackSet.size === 0) {
1554
+ this.callbacks.delete(normalizedPath);
1555
+ }
1556
+ }
1557
+ const watcherInfo = this.watchers.get(normalizedPath);
1558
+ if (watcherInfo) {
1559
+ watcherInfo.refCount--;
1560
+ if (watcherInfo.refCount <= 0) {
1561
+ watcherInfo.watcher.close();
1562
+ this.watchers.delete(normalizedPath);
1563
+ }
1564
+ }
1565
+ }
1566
+ /**
1567
+ * 关闭所有监听器
1568
+ */
1569
+ closeAll() {
1570
+ for (const [, watcherInfo] of this.watchers) {
1571
+ watcherInfo.watcher.close();
1572
+ }
1573
+ this.watchers.clear();
1574
+ this.callbacks.clear();
1575
+ }
1576
+ };
1577
+ var globalWatchManager = null;
1578
+ function getWatchManager() {
1579
+ if (!globalWatchManager) {
1580
+ globalWatchManager = new WatchManager();
1581
+ }
1582
+ return globalWatchManager;
1583
+ }
1584
+
982
1585
  // src/thumbnail/service.ts
983
- var import_node_fs10 = require("fs");
1586
+ var import_node_fs11 = require("fs");
984
1587
  var import_node_path10 = __toESM(require("path"), 1);
985
- var IMAGE_EXTENSIONS2 = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
986
- var VIDEO_EXTENSIONS2 = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"];
1588
+ function isImageFile(filePath, fileType) {
1589
+ return fileType === "image" /* IMAGE */;
1590
+ }
1591
+ function isVideoFile(filePath, fileType) {
1592
+ return fileType === "video" /* VIDEO */;
1593
+ }
987
1594
  var ThumbnailService = class {
988
1595
  database;
989
1596
  imageProcessor;
@@ -1002,7 +1609,7 @@ var ThumbnailService = class {
1002
1609
  */
1003
1610
  async getCachedThumbnailUrl(filePath) {
1004
1611
  try {
1005
- const stats = await import_node_fs10.promises.stat(filePath);
1612
+ const stats = await import_node_fs11.promises.stat(filePath);
1006
1613
  const fileType = getFileType(filePath, stats);
1007
1614
  if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
1008
1615
  return await this.getApplicationIcon(filePath);
@@ -1015,7 +1622,7 @@ var ThumbnailService = class {
1015
1622
  if (cachedPath) {
1016
1623
  return this.urlEncoder(cachedPath);
1017
1624
  }
1018
- getFileHash(filePath).then((fileHash) => {
1625
+ getFileHash(filePath, stats).then((fileHash) => {
1019
1626
  this.generateThumbnail(filePath, fileHash, mtime).catch(() => {
1020
1627
  });
1021
1628
  }).catch(() => {
@@ -1030,7 +1637,7 @@ var ThumbnailService = class {
1030
1637
  */
1031
1638
  async getThumbnailUrl(filePath) {
1032
1639
  try {
1033
- const stats = await import_node_fs10.promises.stat(filePath);
1640
+ const stats = await import_node_fs11.promises.stat(filePath);
1034
1641
  const fileType = getFileType(filePath, stats);
1035
1642
  if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
1036
1643
  return await this.getApplicationIcon(filePath);
@@ -1043,7 +1650,7 @@ var ThumbnailService = class {
1043
1650
  if (cachedPath) {
1044
1651
  return this.urlEncoder(cachedPath);
1045
1652
  }
1046
- const fileHash = await getFileHash(filePath);
1653
+ const fileHash = await getFileHash(filePath, stats);
1047
1654
  const thumbnailPath = await this.generateThumbnail(filePath, fileHash, mtime);
1048
1655
  if (thumbnailPath) {
1049
1656
  return this.urlEncoder(thumbnailPath);
@@ -1063,13 +1670,14 @@ var ThumbnailService = class {
1063
1670
  return cachedPath;
1064
1671
  }
1065
1672
  try {
1066
- const ext = import_node_path10.default.extname(filePath).toLowerCase();
1673
+ const stats = await import_node_fs11.promises.stat(filePath);
1674
+ const fileType = getFileType(filePath, stats);
1067
1675
  const hashPrefix = fileHash.substring(0, 16);
1068
1676
  const thumbnailFileName = `${hashPrefix}.jpg`;
1069
1677
  const thumbnailPath = import_node_path10.default.join(this.database.getCacheDir(), thumbnailFileName);
1070
- if (IMAGE_EXTENSIONS2.includes(ext) && this.imageProcessor) {
1678
+ if (isImageFile(filePath, fileType) && this.imageProcessor) {
1071
1679
  await this.imageProcessor.resize(filePath, thumbnailPath, 256);
1072
- } else if (VIDEO_EXTENSIONS2.includes(ext) && this.videoProcessor) {
1680
+ } else if (isVideoFile(filePath, fileType) && this.videoProcessor) {
1073
1681
  await this.videoProcessor.screenshot(filePath, thumbnailPath, "00:00:01", "256x256");
1074
1682
  } else {
1075
1683
  return null;
@@ -1082,12 +1690,20 @@ var ThumbnailService = class {
1082
1690
  }
1083
1691
  }
1084
1692
  /**
1085
- * 批量生成缩略图
1693
+ * 批量生成缩略图(带并发限制,避免资源耗尽)
1086
1694
  */
1087
- async generateThumbnailsBatch(files) {
1088
- await Promise.allSettled(
1089
- files.map((file) => this.generateThumbnail(file.path, file.hash, file.mtime))
1090
- );
1695
+ async generateThumbnailsBatch(files, concurrency = 3) {
1696
+ const execute = async (file) => {
1697
+ try {
1698
+ await this.generateThumbnail(file.path, file.hash, file.mtime);
1699
+ } catch (error) {
1700
+ console.debug(`Failed to generate thumbnail for ${file.path}:`, error);
1701
+ }
1702
+ };
1703
+ for (let i = 0; i < files.length; i += concurrency) {
1704
+ const batch = files.slice(i, i + concurrency);
1705
+ await Promise.allSettled(batch.map(execute));
1706
+ }
1091
1707
  }
1092
1708
  /**
1093
1709
  * 删除缩略图
@@ -1114,14 +1730,19 @@ function getThumbnailService() {
1114
1730
  // src/thumbnail/database.ts
1115
1731
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1116
1732
  var import_node_path11 = __toESM(require("path"), 1);
1117
- var import_node_fs11 = require("fs");
1733
+ var import_node_fs12 = require("fs");
1118
1734
  var SqliteThumbnailDatabase = class {
1119
1735
  db = null;
1120
1736
  cacheDir;
1121
- constructor(userDataPath) {
1122
- this.cacheDir = import_node_path11.default.join(userDataPath, "cache");
1123
- if (!(0, import_node_fs11.existsSync)(this.cacheDir)) {
1124
- (0, import_node_fs11.mkdirSync)(this.cacheDir, { recursive: true });
1737
+ dbPath;
1738
+ constructor(userDataPath, options = {}) {
1739
+ const defaultDirName = options.dirName || "thumbnails";
1740
+ const defaultDbFileName = options.dbFileName || "thumbnails.db";
1741
+ const inferredDirFromDbPath = options.dbPath ? import_node_path11.default.dirname(options.dbPath) : null;
1742
+ this.cacheDir = options.thumbnailDir ? options.thumbnailDir : inferredDirFromDbPath || import_node_path11.default.join(userDataPath, defaultDirName);
1743
+ this.dbPath = options.dbPath ? options.dbPath : import_node_path11.default.join(this.cacheDir, defaultDbFileName);
1744
+ if (!(0, import_node_fs12.existsSync)(this.cacheDir)) {
1745
+ (0, import_node_fs12.mkdirSync)(this.cacheDir, { recursive: true });
1125
1746
  }
1126
1747
  }
1127
1748
  /**
@@ -1129,8 +1750,7 @@ var SqliteThumbnailDatabase = class {
1129
1750
  */
1130
1751
  init() {
1131
1752
  if (this.db) return;
1132
- const dbPath = import_node_path11.default.join(this.cacheDir, "thumbnails.db");
1133
- this.db = new import_better_sqlite3.default(dbPath, {
1753
+ this.db = new import_better_sqlite3.default(this.dbPath, {
1134
1754
  fileMustExist: false
1135
1755
  });
1136
1756
  this.db.pragma("journal_mode = WAL");
@@ -1162,7 +1782,7 @@ var SqliteThumbnailDatabase = class {
1162
1782
  WHERE file_path = ? AND mtime = ?
1163
1783
  `);
1164
1784
  const row = stmt.get(filePath, mtime);
1165
- if (row && (0, import_node_fs11.existsSync)(row.thumbnail_path)) {
1785
+ if (row && (0, import_node_fs12.existsSync)(row.thumbnail_path)) {
1166
1786
  return row.thumbnail_path;
1167
1787
  }
1168
1788
  return null;
@@ -1175,7 +1795,7 @@ var SqliteThumbnailDatabase = class {
1175
1795
  WHERE file_path = ? AND file_hash = ?
1176
1796
  `);
1177
1797
  const row = stmt.get(filePath, fileHash);
1178
- if (row && row.mtime === mtime && (0, import_node_fs11.existsSync)(row.thumbnail_path)) {
1798
+ if (row && row.mtime === mtime && (0, import_node_fs12.existsSync)(row.thumbnail_path)) {
1179
1799
  return row.thumbnail_path;
1180
1800
  }
1181
1801
  return null;
@@ -1202,15 +1822,19 @@ var SqliteThumbnailDatabase = class {
1202
1822
  }
1203
1823
  close() {
1204
1824
  if (this.db) {
1825
+ try {
1826
+ this.db.pragma("wal_checkpoint(TRUNCATE)");
1827
+ } catch (error) {
1828
+ }
1205
1829
  this.db.close();
1206
1830
  this.db = null;
1207
1831
  }
1208
1832
  }
1209
1833
  };
1210
1834
  var thumbnailDb = null;
1211
- function createSqliteThumbnailDatabase(userDataPath) {
1835
+ function createSqliteThumbnailDatabase(options) {
1212
1836
  if (!thumbnailDb) {
1213
- thumbnailDb = new SqliteThumbnailDatabase(userDataPath);
1837
+ thumbnailDb = new SqliteThumbnailDatabase(options.userDataPath, options);
1214
1838
  thumbnailDb.init();
1215
1839
  }
1216
1840
  return thumbnailDb;
@@ -1218,59 +1842,699 @@ function createSqliteThumbnailDatabase(userDataPath) {
1218
1842
  function getSqliteThumbnailDatabase() {
1219
1843
  return thumbnailDb;
1220
1844
  }
1845
+ function closeThumbnailDatabase() {
1846
+ if (thumbnailDb) {
1847
+ thumbnailDb.close();
1848
+ thumbnailDb = null;
1849
+ }
1850
+ }
1221
1851
 
1222
1852
  // src/thumbnail/processors.ts
1223
- var import_node_child_process2 = require("child_process");
1224
- var import_node_util2 = require("util");
1225
- var execFileAsync = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
1853
+ var import_node_child_process3 = require("child_process");
1226
1854
  function createSharpImageProcessor(sharp) {
1227
1855
  return {
1228
1856
  async resize(filePath, outputPath, size) {
1229
- await sharp(filePath).resize(size, size, {
1230
- fit: "inside",
1857
+ await sharp(filePath).resize({
1858
+ width: size,
1231
1859
  withoutEnlargement: true
1232
- }).jpeg({ quality: 85 }).toFile(outputPath);
1860
+ }).jpeg({
1861
+ quality: 80,
1862
+ optimiseCoding: true
1863
+ // 优化编码,提升压缩率
1864
+ }).toFile(outputPath);
1233
1865
  }
1234
1866
  };
1235
1867
  }
1236
1868
  function createFfmpegVideoProcessor(ffmpegPath) {
1237
1869
  return {
1238
1870
  async screenshot(filePath, outputPath, timestamp, size) {
1239
- const scaleSize = size.replace("x", ":");
1240
- await execFileAsync(ffmpegPath, [
1241
- "-i",
1242
- filePath,
1243
- "-ss",
1244
- timestamp,
1245
- "-vframes",
1246
- "1",
1247
- "-vf",
1248
- `scale=${scaleSize}:force_original_aspect_ratio=decrease`,
1249
- "-y",
1250
- outputPath
1251
- ]);
1871
+ const width = size.split("x")[0];
1872
+ return new Promise((resolve, reject) => {
1873
+ const ffmpeg = (0, import_node_child_process3.spawn)(ffmpegPath, [
1874
+ "-y",
1875
+ "-ss",
1876
+ timestamp,
1877
+ "-i",
1878
+ filePath,
1879
+ "-vframes",
1880
+ "1",
1881
+ "-vf",
1882
+ `thumbnail,scale=${width}:-1:force_original_aspect_ratio=decrease`,
1883
+ // 使用 mjpeg 编码器,性能更好(借鉴 pixflow)
1884
+ "-c:v",
1885
+ "mjpeg",
1886
+ "-q:v",
1887
+ "6",
1888
+ // 质量参数,6 表示中等质量(范围 2-31,数值越小质量越高)
1889
+ outputPath
1890
+ ]);
1891
+ const timeout = setTimeout(() => {
1892
+ ffmpeg.kill();
1893
+ reject(new Error("Video thumbnail generation timeout"));
1894
+ }, 3e4);
1895
+ ffmpeg.stderr.on("data", (data) => {
1896
+ const output = data.toString();
1897
+ if (output.includes("Unsupported pixel format")) {
1898
+ return;
1899
+ }
1900
+ });
1901
+ ffmpeg.on("close", (code) => {
1902
+ clearTimeout(timeout);
1903
+ if (code === 0) {
1904
+ resolve();
1905
+ } else {
1906
+ reject(new Error(`ffmpeg exited with code ${code}`));
1907
+ }
1908
+ });
1909
+ ffmpeg.on("error", (error) => {
1910
+ clearTimeout(timeout);
1911
+ reject(error);
1912
+ });
1913
+ });
1914
+ }
1915
+ };
1916
+ }
1917
+
1918
+ // src/media/format-detector.ts
1919
+ var import_node_child_process4 = require("child_process");
1920
+ var import_node_util3 = require("util");
1921
+ var import_node_path12 = __toESM(require("path"), 1);
1922
+ var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
1923
+ var BROWSER_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["mp4", "webm", "ogg", "ogv"]);
1924
+ var BROWSER_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "vp8", "vp9", "theora", "av1"]);
1925
+ var BROWSER_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "mp3", "opus", "vorbis", "flac"]);
1926
+ var BROWSER_AUDIO_CONTAINERS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "oga", "webm", "m4a", "aac", "flac"]);
1927
+ var BROWSER_AUDIO_ONLY_CODECS = /* @__PURE__ */ new Set(["mp3", "aac", "opus", "vorbis", "flac", "pcm_s16le", "pcm_s24le"]);
1928
+ var REMUXABLE_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "hevc", "h265"]);
1929
+ var REMUXABLE_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "alac"]);
1930
+ var VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
1931
+ "mp4",
1932
+ "mkv",
1933
+ "avi",
1934
+ "mov",
1935
+ "wmv",
1936
+ "flv",
1937
+ "webm",
1938
+ "ogv",
1939
+ "ogg",
1940
+ "m4v",
1941
+ "mpeg",
1942
+ "mpg",
1943
+ "3gp",
1944
+ "ts",
1945
+ "mts",
1946
+ "m2ts",
1947
+ "vob",
1948
+ "rmvb",
1949
+ "rm"
1950
+ ]);
1951
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
1952
+ "mp3",
1953
+ "wav",
1954
+ "flac",
1955
+ "aac",
1956
+ "m4a",
1957
+ "ogg",
1958
+ "oga",
1959
+ "wma",
1960
+ "ape",
1961
+ "alac",
1962
+ "aiff",
1963
+ "aif",
1964
+ "opus",
1965
+ "mid",
1966
+ "midi",
1967
+ "wv",
1968
+ "mka"
1969
+ ]);
1970
+ function getMediaTypeByExtension(filePath) {
1971
+ const ext = import_node_path12.default.extname(filePath).toLowerCase().slice(1);
1972
+ if (VIDEO_EXTENSIONS2.has(ext)) return "video";
1973
+ if (AUDIO_EXTENSIONS.has(ext)) return "audio";
1974
+ return null;
1975
+ }
1976
+ async function getMediaFormat(filePath, ffprobePath) {
1977
+ try {
1978
+ const { stdout } = await execFileAsync(ffprobePath, [
1979
+ "-v",
1980
+ "quiet",
1981
+ "-print_format",
1982
+ "json",
1983
+ "-show_format",
1984
+ "-show_streams",
1985
+ filePath
1986
+ ]);
1987
+ const data = JSON.parse(stdout);
1988
+ const format = data.format || {};
1989
+ const streams = data.streams || [];
1990
+ const videoStream = streams.find((s) => s.codec_type === "video");
1991
+ const audioStream = streams.find((s) => s.codec_type === "audio");
1992
+ const type = videoStream ? "video" : "audio";
1993
+ const formatName = format.format_name || "";
1994
+ const container = formatName.split(",")[0].toLowerCase();
1995
+ return {
1996
+ type,
1997
+ container,
1998
+ videoCodec: videoStream?.codec_name?.toLowerCase(),
1999
+ audioCodec: audioStream?.codec_name?.toLowerCase(),
2000
+ duration: parseFloat(format.duration) || void 0,
2001
+ width: videoStream?.width,
2002
+ height: videoStream?.height,
2003
+ bitrate: parseInt(format.bit_rate) || void 0
2004
+ };
2005
+ } catch {
2006
+ const type = getMediaTypeByExtension(filePath);
2007
+ if (!type) return null;
2008
+ const ext = import_node_path12.default.extname(filePath).toLowerCase().slice(1);
2009
+ return {
2010
+ type,
2011
+ container: ext
2012
+ };
2013
+ }
2014
+ }
2015
+ function canPlayVideoDirectly(format) {
2016
+ if (!BROWSER_VIDEO_CONTAINERS.has(format.container)) {
2017
+ return false;
2018
+ }
2019
+ if (format.videoCodec && !BROWSER_VIDEO_CODECS.has(format.videoCodec)) {
2020
+ return false;
2021
+ }
2022
+ if (format.audioCodec && !BROWSER_AUDIO_CODECS.has(format.audioCodec)) {
2023
+ return false;
2024
+ }
2025
+ return true;
2026
+ }
2027
+ function canPlayAudioDirectly(format) {
2028
+ if (!BROWSER_AUDIO_CONTAINERS.has(format.container)) {
2029
+ return false;
2030
+ }
2031
+ if (format.audioCodec && !BROWSER_AUDIO_ONLY_CODECS.has(format.audioCodec)) {
2032
+ return false;
2033
+ }
2034
+ return true;
2035
+ }
2036
+ function canRemuxVideo(format) {
2037
+ if (!format.videoCodec || !REMUXABLE_VIDEO_CODECS.has(format.videoCodec)) {
2038
+ return false;
2039
+ }
2040
+ if (format.audioCodec) {
2041
+ const audioOk = BROWSER_AUDIO_CODECS.has(format.audioCodec) || REMUXABLE_AUDIO_CODECS.has(format.audioCodec);
2042
+ if (!audioOk) {
2043
+ return false;
2044
+ }
2045
+ }
2046
+ return true;
2047
+ }
2048
+ function canRemuxAudio(format) {
2049
+ return format.audioCodec ? REMUXABLE_AUDIO_CODECS.has(format.audioCodec) : false;
2050
+ }
2051
+ function estimateTranscodeTime(duration, method) {
2052
+ if (!duration || method === "direct") return void 0;
2053
+ if (method === "remux") {
2054
+ return Math.ceil(duration / 50);
2055
+ }
2056
+ return Math.ceil(duration / 3);
2057
+ }
2058
+ async function detectTranscodeNeeds(filePath, ffprobePath) {
2059
+ const formatInfo = await getMediaFormat(filePath, ffprobePath);
2060
+ if (!formatInfo) {
2061
+ const type2 = getMediaTypeByExtension(filePath) || "video";
2062
+ return {
2063
+ type: type2,
2064
+ needsTranscode: false,
2065
+ method: "direct"
2066
+ };
2067
+ }
2068
+ const { type } = formatInfo;
2069
+ if (type === "video") {
2070
+ if (canPlayVideoDirectly(formatInfo)) {
2071
+ return {
2072
+ type,
2073
+ needsTranscode: false,
2074
+ method: "direct",
2075
+ formatInfo
2076
+ };
2077
+ }
2078
+ if (canRemuxVideo(formatInfo)) {
2079
+ return {
2080
+ type,
2081
+ needsTranscode: true,
2082
+ method: "remux",
2083
+ formatInfo,
2084
+ targetFormat: "mp4",
2085
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
2086
+ };
1252
2087
  }
2088
+ return {
2089
+ type,
2090
+ needsTranscode: true,
2091
+ method: "transcode",
2092
+ formatInfo,
2093
+ targetFormat: "mp4",
2094
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
2095
+ };
2096
+ }
2097
+ if (canPlayAudioDirectly(formatInfo)) {
2098
+ return {
2099
+ type,
2100
+ needsTranscode: false,
2101
+ method: "direct",
2102
+ formatInfo
2103
+ };
2104
+ }
2105
+ if (canRemuxAudio(formatInfo)) {
2106
+ return {
2107
+ type,
2108
+ needsTranscode: true,
2109
+ method: "remux",
2110
+ formatInfo,
2111
+ targetFormat: "m4a",
2112
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
2113
+ };
2114
+ }
2115
+ return {
2116
+ type,
2117
+ needsTranscode: true,
2118
+ method: "transcode",
2119
+ formatInfo,
2120
+ targetFormat: "mp3",
2121
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
2122
+ };
2123
+ }
2124
+
2125
+ // src/media/transcoder.ts
2126
+ var import_node_child_process5 = require("child_process");
2127
+ var import_promises3 = __toESM(require("fs/promises"), 1);
2128
+ var import_node_path13 = __toESM(require("path"), 1);
2129
+ var import_node_os3 = __toESM(require("os"), 1);
2130
+ async function getTempOutputPath(sourceFile, targetFormat, tempDir) {
2131
+ const dir = tempDir || import_node_path13.default.join(import_node_os3.default.tmpdir(), "file-explorer-media");
2132
+ await import_promises3.default.mkdir(dir, { recursive: true });
2133
+ const baseName = import_node_path13.default.basename(sourceFile, import_node_path13.default.extname(sourceFile));
2134
+ const timestamp = Date.now();
2135
+ const outputName = `${baseName}_${timestamp}.${targetFormat}`;
2136
+ return import_node_path13.default.join(dir, outputName);
2137
+ }
2138
+ function parseProgress(stderr, duration) {
2139
+ const timeMatch = stderr.match(/time=(\d+):(\d+):(\d+)\.(\d+)/);
2140
+ if (!timeMatch) return null;
2141
+ const hours = parseInt(timeMatch[1]);
2142
+ const minutes = parseInt(timeMatch[2]);
2143
+ const seconds = parseInt(timeMatch[3]);
2144
+ const ms = parseInt(timeMatch[4]);
2145
+ const currentTime = hours * 3600 + minutes * 60 + seconds + ms / 100;
2146
+ const speedMatch = stderr.match(/speed=\s*([\d.]+)x/);
2147
+ const speed = speedMatch ? `${speedMatch[1]}x` : void 0;
2148
+ let percent = 0;
2149
+ if (duration && duration > 0) {
2150
+ percent = Math.min(100, Math.round(currentTime / duration * 100));
2151
+ }
2152
+ return {
2153
+ percent,
2154
+ time: currentTime,
2155
+ duration,
2156
+ speed
1253
2157
  };
1254
2158
  }
2159
+ async function remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2160
+ return new Promise((resolve, reject) => {
2161
+ const args = [
2162
+ "-i",
2163
+ inputPath,
2164
+ "-c",
2165
+ "copy",
2166
+ // 复制流,不重新编码
2167
+ "-movflags",
2168
+ "+faststart",
2169
+ // 优化 MP4 播放
2170
+ "-y",
2171
+ // 覆盖输出文件
2172
+ outputPath
2173
+ ];
2174
+ const ffmpeg = (0, import_node_child_process5.spawn)(ffmpegPath, args);
2175
+ let stderrBuffer = "";
2176
+ ffmpeg.stderr.on("data", (data) => {
2177
+ stderrBuffer += data.toString();
2178
+ if (onProgress) {
2179
+ const progress = parseProgress(stderrBuffer, duration);
2180
+ if (progress) {
2181
+ onProgress(progress);
2182
+ }
2183
+ }
2184
+ });
2185
+ ffmpeg.on("close", (code) => {
2186
+ if (code === 0) {
2187
+ resolve();
2188
+ } else {
2189
+ reject(new Error(`ffmpeg exited with code ${code}`));
2190
+ }
2191
+ });
2192
+ ffmpeg.on("error", reject);
2193
+ });
2194
+ }
2195
+ async function transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2196
+ return new Promise((resolve, reject) => {
2197
+ const args = [
2198
+ "-i",
2199
+ inputPath,
2200
+ "-c:v",
2201
+ "libx264",
2202
+ // H.264 编码
2203
+ "-preset",
2204
+ "fast",
2205
+ // 编码速度预设
2206
+ "-crf",
2207
+ "23",
2208
+ // 质量(18-28,越小越好)
2209
+ "-c:a",
2210
+ "aac",
2211
+ // AAC 音频
2212
+ "-b:a",
2213
+ "192k",
2214
+ // 音频比特率
2215
+ "-movflags",
2216
+ "+faststart",
2217
+ "-y",
2218
+ outputPath
2219
+ ];
2220
+ const ffmpeg = (0, import_node_child_process5.spawn)(ffmpegPath, args);
2221
+ let stderrBuffer = "";
2222
+ ffmpeg.stderr.on("data", (data) => {
2223
+ stderrBuffer += data.toString();
2224
+ if (onProgress) {
2225
+ const progress = parseProgress(stderrBuffer, duration);
2226
+ if (progress) {
2227
+ onProgress(progress);
2228
+ }
2229
+ }
2230
+ });
2231
+ ffmpeg.on("close", (code) => {
2232
+ if (code === 0) {
2233
+ resolve();
2234
+ } else {
2235
+ reject(new Error(`ffmpeg exited with code ${code}`));
2236
+ }
2237
+ });
2238
+ ffmpeg.on("error", reject);
2239
+ });
2240
+ }
2241
+ async function remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2242
+ return new Promise((resolve, reject) => {
2243
+ const args = [
2244
+ "-i",
2245
+ inputPath,
2246
+ "-c",
2247
+ "copy",
2248
+ "-y",
2249
+ outputPath
2250
+ ];
2251
+ const ffmpeg = (0, import_node_child_process5.spawn)(ffmpegPath, args);
2252
+ let stderrBuffer = "";
2253
+ ffmpeg.stderr.on("data", (data) => {
2254
+ stderrBuffer += data.toString();
2255
+ if (onProgress) {
2256
+ const progress = parseProgress(stderrBuffer, duration);
2257
+ if (progress) {
2258
+ onProgress(progress);
2259
+ }
2260
+ }
2261
+ });
2262
+ ffmpeg.on("close", (code) => {
2263
+ if (code === 0) {
2264
+ resolve();
2265
+ } else {
2266
+ reject(new Error(`ffmpeg exited with code ${code}`));
2267
+ }
2268
+ });
2269
+ ffmpeg.on("error", reject);
2270
+ });
2271
+ }
2272
+ async function transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2273
+ return new Promise((resolve, reject) => {
2274
+ const ext = import_node_path13.default.extname(outputPath).toLowerCase();
2275
+ const isM4a = ext === ".m4a";
2276
+ const args = [
2277
+ "-i",
2278
+ inputPath,
2279
+ "-c:a",
2280
+ isM4a ? "aac" : "libmp3lame",
2281
+ "-b:a",
2282
+ "192k",
2283
+ "-y",
2284
+ outputPath
2285
+ ];
2286
+ const ffmpeg = (0, import_node_child_process5.spawn)(ffmpegPath, args);
2287
+ let stderrBuffer = "";
2288
+ ffmpeg.stderr.on("data", (data) => {
2289
+ stderrBuffer += data.toString();
2290
+ if (onProgress) {
2291
+ const progress = parseProgress(stderrBuffer, duration);
2292
+ if (progress) {
2293
+ onProgress(progress);
2294
+ }
2295
+ }
2296
+ });
2297
+ ffmpeg.on("close", (code) => {
2298
+ if (code === 0) {
2299
+ resolve();
2300
+ } else {
2301
+ reject(new Error(`ffmpeg exited with code ${code}`));
2302
+ }
2303
+ });
2304
+ ffmpeg.on("error", reject);
2305
+ });
2306
+ }
2307
+ async function transcodeMedia(ffmpegPath, inputPath, transcodeInfo, tempDir, onProgress) {
2308
+ try {
2309
+ if (!transcodeInfo.needsTranscode) {
2310
+ return {
2311
+ success: true,
2312
+ outputPath: inputPath
2313
+ };
2314
+ }
2315
+ const targetFormat = transcodeInfo.targetFormat || (transcodeInfo.type === "video" ? "mp4" : "mp3");
2316
+ const outputPath = await getTempOutputPath(inputPath, targetFormat, tempDir);
2317
+ const duration = transcodeInfo.formatInfo?.duration;
2318
+ if (transcodeInfo.type === "video") {
2319
+ if (transcodeInfo.method === "remux") {
2320
+ await remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
2321
+ } else {
2322
+ await transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
2323
+ }
2324
+ } else {
2325
+ if (transcodeInfo.method === "remux") {
2326
+ await remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
2327
+ } else {
2328
+ await transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
2329
+ }
2330
+ }
2331
+ if (onProgress) {
2332
+ onProgress({ percent: 100, duration });
2333
+ }
2334
+ return {
2335
+ success: true,
2336
+ outputPath
2337
+ };
2338
+ } catch (error) {
2339
+ return {
2340
+ success: false,
2341
+ error: error instanceof Error ? error.message : "Unknown error"
2342
+ };
2343
+ }
2344
+ }
2345
+ async function cleanupTranscodedFile(filePath) {
2346
+ try {
2347
+ if (filePath.includes("file-explorer-media")) {
2348
+ await import_promises3.default.unlink(filePath);
2349
+ }
2350
+ } catch {
2351
+ }
2352
+ }
2353
+ async function cleanupAllTranscodedFiles(tempDir) {
2354
+ const dir = tempDir || import_node_path13.default.join(import_node_os3.default.tmpdir(), "file-explorer-media");
2355
+ try {
2356
+ const files = await import_promises3.default.readdir(dir);
2357
+ await Promise.all(
2358
+ files.map((file) => import_promises3.default.unlink(import_node_path13.default.join(dir, file)).catch(() => {
2359
+ }))
2360
+ );
2361
+ } catch {
2362
+ }
2363
+ }
2364
+
2365
+ // src/media/service.ts
2366
+ var import_node_path14 = __toESM(require("path"), 1);
2367
+ var import_node_child_process6 = require("child_process");
2368
+ var import_node_util4 = require("util");
2369
+ var execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
2370
+ var mediaServiceInstance = null;
2371
+ var MediaService = class {
2372
+ ffmpegPath;
2373
+ ffprobePath;
2374
+ tempDir;
2375
+ urlEncoder;
2376
+ // 缓存转码信息,避免重复检测
2377
+ transcodeInfoCache = /* @__PURE__ */ new Map();
2378
+ // 缓存已转码文件路径
2379
+ transcodedFiles = /* @__PURE__ */ new Map();
2380
+ constructor(options) {
2381
+ this.ffmpegPath = options.ffmpegPath;
2382
+ this.ffprobePath = options.ffprobePath || import_node_path14.default.join(import_node_path14.default.dirname(options.ffmpegPath), "ffprobe");
2383
+ this.tempDir = options.tempDir;
2384
+ this.urlEncoder = options.urlEncoder;
2385
+ }
2386
+ /**
2387
+ * 检测文件是否需要转码
2388
+ */
2389
+ async needsTranscode(filePath) {
2390
+ const cached = this.transcodeInfoCache.get(filePath);
2391
+ if (cached) {
2392
+ return cached;
2393
+ }
2394
+ const info = await detectTranscodeNeeds(filePath, this.ffprobePath);
2395
+ this.transcodeInfoCache.set(filePath, info);
2396
+ return info;
2397
+ }
2398
+ /**
2399
+ * 执行转码并返回可播放的 URL
2400
+ */
2401
+ async transcode(filePath, onProgress) {
2402
+ const existingOutput = this.transcodedFiles.get(filePath);
2403
+ if (existingOutput) {
2404
+ return {
2405
+ success: true,
2406
+ outputPath: existingOutput,
2407
+ url: this.urlEncoder ? this.urlEncoder(existingOutput) : `file://${existingOutput}`
2408
+ };
2409
+ }
2410
+ const transcodeInfo = await this.needsTranscode(filePath);
2411
+ if (!transcodeInfo.needsTranscode) {
2412
+ const url = this.urlEncoder ? this.urlEncoder(filePath) : `file://${filePath}`;
2413
+ return {
2414
+ success: true,
2415
+ outputPath: filePath,
2416
+ url
2417
+ };
2418
+ }
2419
+ const result = await transcodeMedia(
2420
+ this.ffmpegPath,
2421
+ filePath,
2422
+ transcodeInfo,
2423
+ this.tempDir,
2424
+ onProgress
2425
+ );
2426
+ if (result.success && result.outputPath) {
2427
+ this.transcodedFiles.set(filePath, result.outputPath);
2428
+ result.url = this.urlEncoder ? this.urlEncoder(result.outputPath) : `file://${result.outputPath}`;
2429
+ }
2430
+ return result;
2431
+ }
2432
+ /**
2433
+ * 获取媒体元数据
2434
+ */
2435
+ async getMetadata(filePath) {
2436
+ try {
2437
+ const { stdout } = await execFileAsync2(this.ffprobePath, [
2438
+ "-v",
2439
+ "quiet",
2440
+ "-print_format",
2441
+ "json",
2442
+ "-show_format",
2443
+ "-show_streams",
2444
+ filePath
2445
+ ]);
2446
+ const data = JSON.parse(stdout);
2447
+ const format = data.format || {};
2448
+ const tags = format.tags || {};
2449
+ const formatInfo = await getMediaFormat(filePath, this.ffprobePath);
2450
+ if (!formatInfo) return null;
2451
+ return {
2452
+ filePath,
2453
+ type: formatInfo.type,
2454
+ duration: parseFloat(format.duration) || 0,
2455
+ format: formatInfo,
2456
+ title: tags.title || tags.TITLE,
2457
+ artist: tags.artist || tags.ARTIST,
2458
+ album: tags.album || tags.ALBUM,
2459
+ year: tags.date || tags.DATE || tags.year || tags.YEAR
2460
+ };
2461
+ } catch {
2462
+ return null;
2463
+ }
2464
+ }
2465
+ /**
2466
+ * 获取可播放的 URL
2467
+ * 如果文件需要转码,则执行转码;否则直接返回文件 URL
2468
+ */
2469
+ async getPlayableUrl(filePath, onProgress) {
2470
+ const result = await this.transcode(filePath, onProgress);
2471
+ return result.success ? result.url || null : null;
2472
+ }
2473
+ /**
2474
+ * 清理指定文件的转码缓存
2475
+ */
2476
+ async cleanupFile(filePath) {
2477
+ const transcodedPath = this.transcodedFiles.get(filePath);
2478
+ if (transcodedPath) {
2479
+ await cleanupTranscodedFile(transcodedPath);
2480
+ this.transcodedFiles.delete(filePath);
2481
+ }
2482
+ this.transcodeInfoCache.delete(filePath);
2483
+ }
2484
+ /**
2485
+ * 清理所有转码缓存
2486
+ */
2487
+ async cleanup() {
2488
+ await cleanupAllTranscodedFiles(this.tempDir);
2489
+ this.transcodedFiles.clear();
2490
+ this.transcodeInfoCache.clear();
2491
+ }
2492
+ /**
2493
+ * 清除缓存(不删除文件)
2494
+ */
2495
+ clearCache() {
2496
+ this.transcodeInfoCache.clear();
2497
+ }
2498
+ };
2499
+ function initMediaService(options) {
2500
+ mediaServiceInstance = new MediaService(options);
2501
+ return mediaServiceInstance;
2502
+ }
2503
+ function getMediaService() {
2504
+ return mediaServiceInstance;
2505
+ }
2506
+ function createMediaService(options) {
2507
+ return new MediaService(options);
2508
+ }
1255
2509
  // Annotate the CommonJS export names for ESM import in node:
1256
2510
  0 && (module.exports = {
1257
2511
  APP_PROTOCOL_HOST,
1258
2512
  APP_PROTOCOL_PREFIX,
1259
2513
  APP_PROTOCOL_SCHEME,
1260
2514
  FileType,
2515
+ MediaService,
1261
2516
  SqliteThumbnailDatabase,
1262
2517
  ThumbnailService,
2518
+ WatchManager,
2519
+ cleanupAllTranscodedFiles,
2520
+ cleanupTranscodedFile,
2521
+ closeThumbnailDatabase,
2522
+ compressFiles,
1263
2523
  copyFiles,
1264
2524
  copyFilesToClipboard,
1265
2525
  createFfmpegVideoProcessor,
1266
2526
  createFile,
1267
2527
  createFolder,
2528
+ createMediaService,
1268
2529
  createSharpImageProcessor,
1269
2530
  createSqliteThumbnailDatabase,
1270
2531
  decodeFileUrl,
1271
2532
  deleteFiles,
2533
+ detectArchiveFormat,
2534
+ detectTranscodeNeeds,
1272
2535
  encodeFileUrl,
1273
2536
  exists,
2537
+ extractArchive,
1274
2538
  formatDate,
1275
2539
  formatDateTime,
1276
2540
  formatFileSize,
@@ -1278,28 +2542,39 @@ function createFfmpegVideoProcessor(ffmpegPath) {
1278
2542
  getApplicationIcon,
1279
2543
  getClipboardFiles,
1280
2544
  getFileHash,
1281
- getFileHashes,
1282
2545
  getFileInfo,
1283
2546
  getFileType,
1284
2547
  getHomeDirectory,
2548
+ getMediaFormat,
2549
+ getMediaService,
2550
+ getMediaTypeByExtension,
1285
2551
  getPlatform,
1286
2552
  getSqliteThumbnailDatabase,
1287
2553
  getSystemPath,
1288
2554
  getThumbnailService,
2555
+ getWatchManager,
2556
+ initMediaService,
1289
2557
  initThumbnailService,
1290
2558
  isAppProtocolUrl,
2559
+ isArchiveFile,
1291
2560
  isDirectory,
1292
2561
  isMediaFile,
1293
2562
  isPreviewable,
1294
2563
  moveFiles,
2564
+ openInEditor,
2565
+ openInTerminal,
1295
2566
  pasteFiles,
1296
2567
  readDirectory,
1297
2568
  readFileContent,
1298
2569
  readImageAsBase64,
1299
2570
  renameFile,
2571
+ revealInFileManager,
1300
2572
  searchFiles,
1301
2573
  searchFilesStream,
1302
2574
  searchFilesSync,
2575
+ showFileInfo,
2576
+ transcodeMedia,
2577
+ watchDirectory,
1303
2578
  writeFileContent
1304
2579
  });
1305
2580
  //# sourceMappingURL=index.cjs.map