From 50948ca72ae80d78117d92d4f5a96fa95fa8cb07 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 21 Mar 2026 15:28:14 +0200 Subject: [PATCH 1/2] feat(media-library): add MIME types for mkv, avi, m4a, and svg formats Extend supported media types to include video/matroska, video/x-msvideo, audio/x-m4a, audio/mp4, and image/svg+xml. Improve MIME detection to prefer extension-based lookup when browser-reported type is generic. --- .../media-library/utils/validation.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/features/media-library/utils/validation.ts b/src/features/media-library/utils/validation.ts index a623453d2..64123897f 100644 --- a/src/features/media-library/utils/validation.ts +++ b/src/features/media-library/utils/validation.ts @@ -10,6 +10,8 @@ const SUPPORTED_VIDEO_TYPES = [ 'video/webm', 'video/quicktime', // .mov files 'video/x-matroska', // .mkv files + 'video/matroska', // .mkv files (alternate browser MIME) + 'video/x-msvideo', // .avi files ]; const SUPPORTED_AUDIO_TYPES = [ @@ -17,6 +19,8 @@ const SUPPORTED_AUDIO_TYPES = [ 'audio/mpeg', // MP3 also uses audio/mpeg 'audio/wav', 'audio/aac', + 'audio/x-m4a', // .m4a files + 'audio/mp4', // .m4a also reported as audio/mp4 'audio/ogg', // Opus codec in Ogg container ]; @@ -26,6 +30,7 @@ const SUPPORTED_IMAGE_TYPES = [ 'image/png', 'image/gif', 'image/webp', + 'image/svg+xml', // .svg files ]; const MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024; // 5GB @@ -37,10 +42,12 @@ const EXTENSION_TO_MIME: Record = { '.webm': 'video/webm', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska', + '.avi': 'video/x-msvideo', // Audio '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.aac': 'audio/aac', + '.m4a': 'audio/x-m4a', '.ogg': 'audio/ogg', '.opus': 'audio/ogg', // Image @@ -49,18 +56,21 @@ const EXTENSION_TO_MIME: Record = { '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', + '.svg': 'image/svg+xml', }; /** * Get MIME type from file, falling back to extension-based detection */ export function getMimeType(file: File): string { - if (file.type) { - return file.type; - } - // Fallback to extension-based detection + // Prefer extension-based detection for known extensions — + // browsers sometimes report non-standard MIME types (e.g. "video/matroska" + // instead of "video/x-matroska" for .mkv files). const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]; - return ext ? EXTENSION_TO_MIME[ext] || '' : ''; + if (ext && EXTENSION_TO_MIME[ext]) { + return EXTENSION_TO_MIME[ext]; + } + return file.type || ''; } interface ValidationResult { @@ -98,7 +108,7 @@ export function validateMediaFile(file: File): ValidationResult { if (!allSupportedTypes.includes(mimeType)) { return { valid: false, - error: `Unsupported file type: ${mimeType || file.name.split('.').pop()}. Supported types: video (mp4, webm, mov, mkv), audio (mp3, wav, aac, ogg/opus), image (jpg, png, gif, webp)`, + error: `Unsupported file type: ${mimeType || file.name.split('.').pop()}. Supported types: video (mp4, webm, mov, mkv, avi), audio (mp3, wav, aac, m4a, ogg/opus), image (jpg, png, gif, webp, svg)`, }; } From d33d4fcfecf8404e3fc5c038e39ae5dfd081b2f0 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 21 Mar 2026 15:28:29 +0200 Subject: [PATCH 2/2] feat(media-library): add auto-import from watched directory Add directory watching via File System Access API that polls for new media files and automatically imports them to the library and timeline. Includes stability detection to wait for files still being written. --- .../components/media-library.tsx | 26 ++ .../media-library/deps/timeline-actions.ts | 1 + .../media-library/deps/timeline-utils.ts | 5 + .../media-library/stores/auto-import-store.ts | 272 ++++++++++++++++++ .../timeline/contracts/media-library.ts | 7 + 5 files changed, 311 insertions(+) create mode 100644 src/features/media-library/stores/auto-import-store.ts diff --git a/src/features/media-library/components/media-library.tsx b/src/features/media-library/components/media-library.tsx index fe5a74423..ac39e174e 100644 --- a/src/features/media-library/components/media-library.tsx +++ b/src/features/media-library/components/media-library.tsx @@ -35,6 +35,7 @@ import { MissingMediaDialog } from './missing-media-dialog'; import { OrphanedClipsDialog } from './orphaned-clips-dialog'; import { UnsupportedAudioCodecDialog } from './unsupported-audio-codec-dialog'; import { useMediaLibraryStore } from '../stores/media-library-store'; +import { useAutoImportStore } from '../stores/auto-import-store'; import { useTimelineStore, useCompositionNavigationStore, @@ -111,6 +112,12 @@ export const MediaLibrary = memo(function MediaLibrary({ onMediaSelect }: MediaL const proxyStatus = useMediaLibraryStore((s) => s.proxyStatus); const proxyProgress = useMediaLibraryStore((s) => s.proxyProgress); + // Auto-import state + const autoImportActive = useAutoImportStore((s) => s.active); + const autoImportFolderName = useAutoImportStore((s) => s.folderName); + const enableAutoImport = useAutoImportStore((s) => s.enable); + const disableAutoImport = useAutoImportStore((s) => s.disable); + // Composition navigation — show banner when inside a sub-comp const activeCompositionId = useCompositionNavigationStore((s) => s.activeCompositionId); const breadcrumbs = useCompositionNavigationStore((s) => s.breadcrumbs); @@ -394,6 +401,25 @@ export const MediaLibrary = memo(function MediaLibrary({ onMediaSelect }: MediaL {/* Header toolbar */}
+ {/* Auto-import toggle */} + + {/* Import action */}