@@ -25,6 +25,15 @@ const PROJECT_FILE_EXTENSION = "openscreen";
2525const SHORTCUTS_FILE = path . join ( app . getPath ( "userData" ) , "shortcuts.json" ) ;
2626const RECORDING_SESSION_SUFFIX = ".session.json" ;
2727const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set ( [ ".webm" , ".mp4" , ".mov" , ".avi" , ".mkv" ] ) ;
28+ const ALLOWED_IMPORT_AUDIO_EXTENSIONS = new Set ( [
29+ ".mp3" ,
30+ ".wav" ,
31+ ".m4a" ,
32+ ".aac" ,
33+ ".ogg" ,
34+ ".flac" ,
35+ ".webm" ,
36+ ] ) ;
2837
2938/**
3039 * Paths explicitly approved by the user via file picker dialogs or project loads.
@@ -56,6 +65,10 @@ function hasAllowedImportVideoExtension(filePath: string): boolean {
5665 return ALLOWED_IMPORT_VIDEO_EXTENSIONS . has ( path . extname ( filePath ) . toLowerCase ( ) ) ;
5766}
5867
68+ function hasAllowedImportAudioExtension ( filePath : string ) : boolean {
69+ return ALLOWED_IMPORT_AUDIO_EXTENSIONS . has ( path . extname ( filePath ) . toLowerCase ( ) ) ;
70+ }
71+
5972async function approveReadableVideoPath (
6073 filePath ?: string | null ,
6174 trustedDirs ?: string [ ] ,
@@ -97,6 +110,33 @@ async function approveReadableVideoPath(
97110 return normalizedPath ;
98111}
99112
113+ async function approveReadableAudioPath ( filePath ?: string | null ) : Promise < string | null > {
114+ const normalizedPath = normalizeVideoSourcePath ( filePath ) ;
115+ if ( ! normalizedPath ) {
116+ return null ;
117+ }
118+
119+ if ( isPathAllowed ( normalizedPath ) ) {
120+ return normalizedPath ;
121+ }
122+
123+ if ( ! hasAllowedImportAudioExtension ( normalizedPath ) ) {
124+ return null ;
125+ }
126+
127+ try {
128+ const stats = await fs . stat ( normalizedPath ) ;
129+ if ( ! stats . isFile ( ) ) {
130+ return null ;
131+ }
132+ } catch {
133+ return null ;
134+ }
135+
136+ approveFilePath ( normalizedPath ) ;
137+ return normalizedPath ;
138+ }
139+
100140function resolveRecordingOutputPath ( fileName : string ) : string {
101141 const trimmed = fileName . trim ( ) ;
102142 if ( ! trimmed ) {
@@ -722,6 +762,47 @@ export function registerIpcHandlers(
722762 }
723763 } ) ;
724764
765+ ipcMain . handle ( "open-audio-file-picker" , async ( ) => {
766+ try {
767+ const result = await dialog . showOpenDialog ( {
768+ title : "Select audio" ,
769+ defaultPath : app . getPath ( "music" ) ,
770+ filters : [
771+ {
772+ name : "Audio Files" ,
773+ extensions : [ "mp3" , "wav" , "m4a" , "aac" , "ogg" , "flac" , "webm" ] ,
774+ } ,
775+ { name : "All Files" , extensions : [ "*" ] } ,
776+ ] ,
777+ properties : [ "openFile" ] ,
778+ } ) ;
779+
780+ if ( result . canceled || result . filePaths . length === 0 ) {
781+ return { success : false , canceled : true } ;
782+ }
783+
784+ const approvedPath = await approveReadableAudioPath ( result . filePaths [ 0 ] ) ;
785+ if ( ! approvedPath ) {
786+ return {
787+ success : false ,
788+ message : "Selected file is not a supported audio file" ,
789+ } ;
790+ }
791+
792+ return {
793+ success : true ,
794+ path : approvedPath ,
795+ } ;
796+ } catch ( error ) {
797+ console . error ( "Failed to open audio file picker:" , error ) ;
798+ return {
799+ success : false ,
800+ message : "Failed to open audio file picker" ,
801+ error : String ( error ) ,
802+ } ;
803+ }
804+ } ) ;
805+
725806 ipcMain . handle ( "reveal-in-folder" , async ( _ , filePath : string ) => {
726807 try {
727808 // shell.showItemInFolder doesn't return a value, it throws on error
0 commit comments