Skip to content

feat(media-library): expanded MIME types and auto-import from watched directory#130

Open
shimondoodkin wants to merge 2 commits into
walterlow:mainfrom
shimondoodkin:main
Open

feat(media-library): expanded MIME types and auto-import from watched directory#130
shimondoodkin wants to merge 2 commits into
walterlow:mainfrom
shimondoodkin:main

Conversation

@shimondoodkin
Copy link
Copy Markdown

@shimondoodkin shimondoodkin commented Mar 21, 2026

Summary

  • MIME type expansion: Add support for mkv, avi, m4a, and svg formats with improved extension-based MIME detection fallback
  • Auto-import: Watch a local directory via File System Access API, automatically importing new media files to the library and appending them to the timeline. Includes stability detection to wait for files still being written (e.g. OBS recordings)

Test plan

  • Verify mkv, avi, m4a, and svg files can be imported via the media library
  • Enable auto-import, select a folder, and drop a new media file into it — confirm it appears in the library and timeline
  • Verify files still being written (growing in size) are not imported until stable
  • Disable auto-import and confirm polling stops

Summary by CodeRabbit

  • New Features
    • Auto-import media: Monitor and automatically import media files from a selected directory. Newly detected files are added to the timeline without manual intervention.
    • Extended format support: Added support for MKV and AVI videos, SVG images, and M4A audio files.

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.
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.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 21, 2026

@shimondoodkin is attempting to deploy a commit to the walterlow's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Adds auto-import functionality to the media library that monitors a selected directory for new media files, detects when files reach stable sizes, and automatically imports them into the timeline while expanding supported file formats to include additional video, audio, and image types.

Changes

Cohort / File(s) Summary
Auto-Import Feature
src/features/media-library/components/media-library.tsx, src/features/media-library/stores/auto-import-store.ts
Introduces new Zustand store for polling a user-selected directory, detecting file stability, and importing media into timeline; integrates toggle button in media library header to enable/disable auto-import with dynamic styling based on active state and watched folder.
Media Support Expansion
src/features/media-library/utils/validation.ts
Extends supported MIME types to include .mkv, .avi, .m4a, and .svg formats; updates getMimeType() to prioritize extension-based detection over file type fallback.
Dependency Re-exports
src/features/media-library/deps/timeline-actions.ts, src/features/media-library/deps/timeline-utils.ts, src/features/timeline/contracts/media-library.ts
Expands public API surface by re-exporting addItem, buildDroppedMediaTimelineItem, DroppableMediaType, and getDroppedMediaDurationInFrames from internal modules to support auto-import integration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Media Library UI
    participant Store as Auto-Import Store
    participant DirectoryAPI as Directory Picker API
    participant PointerLoop as Polling Loop
    participant MediaLib as Media Library Store
    participant TimelineStore as Timeline Store

    User->>UI: Click "Enable Auto Import"
    UI->>Store: enable()
    Store->>DirectoryAPI: showDirectoryPicker()
    DirectoryAPI-->>Store: Directory handle
    Store->>Store: Snapshot existing files into knownFiles
    Store->>PointerLoop: Start setInterval polling
    
    loop Every poll interval
        PointerLoop->>Store: Scan directory for new files
        Store->>Store: Check permission & supported extensions
        Store->>Store: Track file size in pendingFiles map
        
        alt File size stable (non-zero & unchanged)
            Store->>MediaLib: Import media file
            MediaLib->>MediaLib: Resolve blob URL & thumbnails
            Store->>TimelineStore: Find first visible unlocked track
            Store->>TimelineStore: Calculate duration in frames
            Store->>TimelineStore: Find track end frame
            Store->>TimelineStore: Build & append timeline item
            Store->>Store: Update knownFiles, emit notification
        else File size changing
            Store->>Store: Wait for next poll
        end
    end
    
    User->>UI: Click "Disable Auto Import"
    UI->>Store: disable()
    Store->>PointerLoop: Clear polling interval
    Store->>Store: Emit disable notification
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A directory watched with patient eyes,
Files emerging, reaching stable size—
Hop by hop they join the timeline's song,
Auto-import carries them along! ✨
Fresh formats welcome, .svg and more,
Media library opens every door.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main changes: expanded MIME type support and auto-import functionality from a watched directory.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable poems in the walkthrough.

Disable the reviews.poem setting to disable the poems in the walkthrough.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 21, 2026

Greptile Summary

This PR extends the media library with two independent improvements: MIME type support for mkv, avi, m4a, and svg formats (including an extension-first fallback in getMimeType), and a new useAutoImportStore that watches a user-selected directory via the File System Access API, detects newly written files, waits for write stability, then imports them into the media library and appends clips to the first available timeline track.

The MIME type additions and the UI wiring are clean and low-risk. The auto-import store logic, however, has two concurrency bugs that can cause duplicate clips to appear on the timeline:

  • Stale knownFiles snapshot: currentKnown is built from a state object captured at the start of the function, but the actual setState call happens many await points later. A concurrent overlapping poll can overwrite this update, causing a file that was just marked "known" to be forgotten and re-imported on the next cycle.
  • No concurrency guard: setInterval fires a new poll every 3 seconds unconditionally. Importing a large video file can easily exceed that, so two polls can overlap in the stability-check and import phases, each observing the same file in the shared pendingFiles map before either one removes it.

Both issues share the same root fix: serialise poll execution with an isRunning flag and replace state.knownFiles with a fresh useAutoImportStore.getState().knownFiles read at the point currentKnown is constructed.

Confidence Score: 3/5

  • The MIME-type expansion is safe to merge, but the auto-import store has two concurrency bugs that can silently place duplicate clips on the timeline under normal usage.
  • Two reproducible logic bugs in the primary new feature (both fixable with small, targeted changes) lower the score from a happy-path 4. The stale-snapshot race and missing concurrency guard affect the main user path (watching a folder for OBS recordings) and will manifest whenever an import takes longer than 3 seconds. The fixes are straightforward and don't require architectural changes.
  • src/features/media-library/stores/auto-import-store.ts — the stale knownFiles snapshot (line 163) and missing concurrency guard (line 81) need to be addressed before merging.

Important Files Changed

Filename Overview
src/features/media-library/stores/auto-import-store.ts New store implementing directory-watching auto-import. Contains two logic bugs: stale knownFiles snapshot on concurrent polls (line 163) and no concurrency guard to prevent overlapping poll executions (line 81), both of which can produce duplicate timeline clips. Module-level pendingFiles map is also outside Zustand state control.
src/features/media-library/utils/validation.ts Adds new MIME types (mkv alternate, avi, m4a, svg) and flips getMimeType to prefer extension-based detection. The extension-first strategy is a minor regression in strictness — misnamed files pass more easily — but the comment acknowledges the client-side spoofing limitation and mediabunny re-validates on decode.
src/features/media-library/components/media-library.tsx Adds Auto Import toggle button wired to useAutoImportStore. UI is straightforward and correctly disabled when no project is open.
src/features/media-library/deps/timeline-actions.ts Exports addItem from the timeline contract so the auto-import store can append items to the timeline.
src/features/media-library/deps/timeline-utils.ts Re-exports buildDroppedMediaTimelineItem, DroppableMediaType, and getDroppedMediaDurationInFrames through the media-library deps layer. Straightforward passthrough.
src/features/timeline/contracts/media-library.ts Extends the timeline→media-library contract to expose addItem, buildDroppedMediaTimelineItem, and related utilities. Clean boundary addition.

Sequence Diagram

sequenceDiagram
    participant UI as MediaLibrary UI
    participant Store as AutoImportStore
    participant Poll as pollForNewFiles (interval)
    participant FS as File System API
    participant ML as MediaLibraryStore
    participant TL as ItemsStore (Timeline)

    UI->>Store: enable()
    Store->>FS: showDirectoryPicker()
    FS-->>Store: directoryHandle
    Store->>FS: dirHandle.values() (snapshot existing files)
    Store->>Store: set({ active, knownFiles, timerId })
    loop Every 3s (setInterval)
        Poll->>FS: queryPermission()
        FS-->>Poll: "granted"
        Poll->>FS: dirHandle.values() (scan for new files)
        FS-->>Poll: entries
        Poll->>Poll: add new entries to pendingFiles (module-level Map)
        loop For each pending file
            Poll->>FS: getFileHandle(name) + getFile()
            FS-->>Poll: current size
            alt size unchanged & > 0
                Poll->>Poll: move to stableHandles
                Poll->>Poll: remove from pendingFiles
            else still growing
                Poll->>Poll: update pendingFiles size
            end
        end
        Poll->>Store: setState({ knownFiles: currentKnown })
        Poll->>ML: importHandles(stableHandles)
        ML-->>Poll: importedMedia[]
        loop For each imported media
            Poll->>TL: getState().items (read end frame)
            Poll->>ML: resolveMediaUrl / getThumbnailBlobUrl
            Poll->>TL: addItem(timelineItem)
        end
    end
    UI->>Store: disable()
    Store->>Store: clearInterval + pendingFiles.clear() + reset state
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 163-196

Comment:
**Stale `knownFiles` snapshot causes lost state on concurrent polls**

`currentKnown` is built from `state.knownFiles` — a snapshot captured at the very top of the function (line 127). Between that snapshot and line 163 several `await` points occur (permission check, full directory scan, per-file `getFile()` calls). If another concurrent poll (which can start after 3 seconds while this one is still running) completes its own stable-file cycle and calls `useAutoImportStore.setState({ knownFiles: ... })` during that window, its additions will be wiped when this poll subsequently calls `setState` with `currentKnown` derived from the stale snapshot.

This means a file that the second poll just marked as "known" (to avoid re-import) will disappear from store state, and the next poll will attempt to import it again — placing a duplicate clip on the timeline.

Fix: re-read the _current_ store state at line 163 instead of using the stale `state` snapshot:

```suggestion
    const stableHandles: FileSystemFileHandle[] = [];
    const currentKnown = new Set(useAutoImportStore.getState().knownFiles);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 81

Comment:
**No concurrency guard — overlapping polls can cause duplicate imports**

`setInterval` fires `pollForNewFiles` every 3 seconds regardless of whether the previous invocation has completed. A single import of a large video file can easily exceed 3 seconds, causing two (or more) polls to run simultaneously. When concurrent polls both observe the same stable file in `pendingFiles` before either one has had a chance to delete it, both polls can each:
1. Call `pendingFiles.delete(fileName)` — only one will "win" the deletion, but the iteration snapshot for the other may already hold the entry.
2. Both call `addItem(timelineItem)` — resulting in a duplicate clip on the timeline.

Add a simple boolean guard to serialise poll invocations:

```
let pollRunning = false;

async function pollForNewFiles(): Promise<void> {
  if (pollRunning) return;
  pollRunning = true;
  try {
    // ... existing body ...
  } finally {
    pollRunning = false;
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 36

Comment:
**Module-level mutable state lives outside the store**

`pendingFiles` is declared at module scope, making it invisible to Zustand devtools, not serialisable/rehydratable, and — most practically — not cleared on a hot-module-reload (HMR) cycle. If the module is re-evaluated, `pendingFiles` is reset but the Zustand store may still be active, silently leaking stale file-size entries. There's also a subtle edge case: if `disable()` is called while `pollForNewFiles` is executing between two `await` points, the `pendingFiles.clear()` on line 113 runs, but the already-captured `for...of` iterator over `pendingFiles` (line 165) will stop iterating mid-loop because Map iterators reflect live deletions — meaning some stable files may never get imported even though they were detected.

Consider moving `pendingFiles` into the Zustand state object (as a `Map<string, number>`), so its lifecycle is fully controlled by `enable`/`disable`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/features/media-library/utils/validation.ts
Line: 65-74

Comment:
**Extension-first detection silently accepts misnamed files**

The updated `getMimeType` now returns the extension-mapped MIME type even when the browser has already reported a valid, different type in `file.type`. This means a file named `not-a-video.mp4` (e.g. a renamed PDF) will pass MIME validation as `video/mp4` because the extension wins over `file.type`. The previous behaviour — trust `file.type` first, fall back to extension only when the browser provides nothing — is generally safer.

The stated goal (normalising `video/matroska``video/x-matroska` for `.mkv` files) could be achieved more narrowly by only applying extension override when the browser-reported type would otherwise fail validation:

```ts
export function getMimeType(file: File): string {
  const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
  const extMime = ext ? EXTENSION_TO_MIME[ext] : undefined;
  // Normalise non-standard browser MIME types (e.g. video/matroska -> video/x-matroska)
  if (file.type && extMime && file.type !== extMime) {
    const allSupported = [...SUPPORTED_VIDEO_TYPES, ...SUPPORTED_AUDIO_TYPES, ...SUPPORTED_IMAGE_TYPES];
    if (!allSupported.includes(file.type)) return extMime;
  }
  return file.type || extMime || '';
}
```

Note: the existing security comment on line 98 already acknowledges client-side MIME spoofing, so this is a best-effort concern, not a blocking one.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "feat(media-library):..."

Comment on lines +163 to +196
const currentKnown = new Set(state.knownFiles);

for (const [fileName, lastSize] of pendingFiles) {
try {
const handle = await state.directoryHandle.getFileHandle(fileName);
const file = await handle.getFile();
const currentSize = file.size;

if (currentSize === 0) {
// Still empty, keep waiting
continue;
}

if (currentSize === lastSize) {
// Size unchanged and > 0 — file is done being written
stableHandles.push(handle);
currentKnown.add(fileName);
pendingFiles.delete(fileName);
logger.info(`Auto-import: "${fileName}" stable at ${currentSize} bytes, importing`);
} else {
// Still growing, update the tracked size for next poll
pendingFiles.set(fileName, currentSize);
logger.info(`Auto-import: "${fileName}" still writing (${lastSize} -> ${currentSize} bytes)`);
}
} catch {
// File may have been deleted before we could read it
pendingFiles.delete(fileName);
}
}

if (stableHandles.length === 0) return;

// Update known files immediately to avoid re-processing
useAutoImportStore.setState({ knownFiles: currentKnown });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale knownFiles snapshot causes lost state on concurrent polls

currentKnown is built from state.knownFiles — a snapshot captured at the very top of the function (line 127). Between that snapshot and line 163 several await points occur (permission check, full directory scan, per-file getFile() calls). If another concurrent poll (which can start after 3 seconds while this one is still running) completes its own stable-file cycle and calls useAutoImportStore.setState({ knownFiles: ... }) during that window, its additions will be wiped when this poll subsequently calls setState with currentKnown derived from the stale snapshot.

This means a file that the second poll just marked as "known" (to avoid re-import) will disappear from store state, and the next poll will attempt to import it again — placing a duplicate clip on the timeline.

Fix: re-read the current store state at line 163 instead of using the stale state snapshot:

Suggested change
const currentKnown = new Set(state.knownFiles);
for (const [fileName, lastSize] of pendingFiles) {
try {
const handle = await state.directoryHandle.getFileHandle(fileName);
const file = await handle.getFile();
const currentSize = file.size;
if (currentSize === 0) {
// Still empty, keep waiting
continue;
}
if (currentSize === lastSize) {
// Size unchanged and > 0 — file is done being written
stableHandles.push(handle);
currentKnown.add(fileName);
pendingFiles.delete(fileName);
logger.info(`Auto-import: "${fileName}" stable at ${currentSize} bytes, importing`);
} else {
// Still growing, update the tracked size for next poll
pendingFiles.set(fileName, currentSize);
logger.info(`Auto-import: "${fileName}" still writing (${lastSize} -> ${currentSize} bytes)`);
}
} catch {
// File may have been deleted before we could read it
pendingFiles.delete(fileName);
}
}
if (stableHandles.length === 0) return;
// Update known files immediately to avoid re-processing
useAutoImportStore.setState({ knownFiles: currentKnown });
const stableHandles: FileSystemFileHandle[] = [];
const currentKnown = new Set(useAutoImportStore.getState().knownFiles);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 163-196

Comment:
**Stale `knownFiles` snapshot causes lost state on concurrent polls**

`currentKnown` is built from `state.knownFiles` — a snapshot captured at the very top of the function (line 127). Between that snapshot and line 163 several `await` points occur (permission check, full directory scan, per-file `getFile()` calls). If another concurrent poll (which can start after 3 seconds while this one is still running) completes its own stable-file cycle and calls `useAutoImportStore.setState({ knownFiles: ... })` during that window, its additions will be wiped when this poll subsequently calls `setState` with `currentKnown` derived from the stale snapshot.

This means a file that the second poll just marked as "known" (to avoid re-import) will disappear from store state, and the next poll will attempt to import it again — placing a duplicate clip on the timeline.

Fix: re-read the _current_ store state at line 163 instead of using the stale `state` snapshot:

```suggestion
    const stableHandles: FileSystemFileHandle[] = [];
    const currentKnown = new Set(useAutoImportStore.getState().knownFiles);
```

How can I resolve this? If you propose a fix, please make it concise.

}
}

const timerId = setInterval(() => pollForNewFiles(), POLL_INTERVAL_MS);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No concurrency guard — overlapping polls can cause duplicate imports

setInterval fires pollForNewFiles every 3 seconds regardless of whether the previous invocation has completed. A single import of a large video file can easily exceed 3 seconds, causing two (or more) polls to run simultaneously. When concurrent polls both observe the same stable file in pendingFiles before either one has had a chance to delete it, both polls can each:

  1. Call pendingFiles.delete(fileName) — only one will "win" the deletion, but the iteration snapshot for the other may already hold the entry.
  2. Both call addItem(timelineItem) — resulting in a duplicate clip on the timeline.

Add a simple boolean guard to serialise poll invocations:

let pollRunning = false;

async function pollForNewFiles(): Promise<void> {
  if (pollRunning) return;
  pollRunning = true;
  try {
    // ... existing body ...
  } finally {
    pollRunning = false;
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 81

Comment:
**No concurrency guard — overlapping polls can cause duplicate imports**

`setInterval` fires `pollForNewFiles` every 3 seconds regardless of whether the previous invocation has completed. A single import of a large video file can easily exceed 3 seconds, causing two (or more) polls to run simultaneously. When concurrent polls both observe the same stable file in `pendingFiles` before either one has had a chance to delete it, both polls can each:
1. Call `pendingFiles.delete(fileName)` — only one will "win" the deletion, but the iteration snapshot for the other may already hold the entry.
2. Both call `addItem(timelineItem)` — resulting in a duplicate clip on the timeline.

Add a simple boolean guard to serialise poll invocations:

```
let pollRunning = false;

async function pollForNewFiles(): Promise<void> {
  if (pollRunning) return;
  pollRunning = true;
  try {
    // ... existing body ...
  } finally {
    pollRunning = false;
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

* Maps filename -> last observed file size. A file is considered stable when its
* size is >0 and unchanged between two consecutive polls.
*/
const pendingFiles = new Map<string, number>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Module-level mutable state lives outside the store

pendingFiles is declared at module scope, making it invisible to Zustand devtools, not serialisable/rehydratable, and — most practically — not cleared on a hot-module-reload (HMR) cycle. If the module is re-evaluated, pendingFiles is reset but the Zustand store may still be active, silently leaking stale file-size entries. There's also a subtle edge case: if disable() is called while pollForNewFiles is executing between two await points, the pendingFiles.clear() on line 113 runs, but the already-captured for...of iterator over pendingFiles (line 165) will stop iterating mid-loop because Map iterators reflect live deletions — meaning some stable files may never get imported even though they were detected.

Consider moving pendingFiles into the Zustand state object (as a Map<string, number>), so its lifecycle is fully controlled by enable/disable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/media-library/stores/auto-import-store.ts
Line: 36

Comment:
**Module-level mutable state lives outside the store**

`pendingFiles` is declared at module scope, making it invisible to Zustand devtools, not serialisable/rehydratable, and — most practically — not cleared on a hot-module-reload (HMR) cycle. If the module is re-evaluated, `pendingFiles` is reset but the Zustand store may still be active, silently leaking stale file-size entries. There's also a subtle edge case: if `disable()` is called while `pollForNewFiles` is executing between two `await` points, the `pendingFiles.clear()` on line 113 runs, but the already-captured `for...of` iterator over `pendingFiles` (line 165) will stop iterating mid-loop because Map iterators reflect live deletions — meaning some stable files may never get imported even though they were detected.

Consider moving `pendingFiles` into the Zustand state object (as a `Map<string, number>`), so its lifecycle is fully controlled by `enable`/`disable`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 65 to 74
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 || '';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Extension-first detection silently accepts misnamed files

The updated getMimeType now returns the extension-mapped MIME type even when the browser has already reported a valid, different type in file.type. This means a file named not-a-video.mp4 (e.g. a renamed PDF) will pass MIME validation as video/mp4 because the extension wins over file.type. The previous behaviour — trust file.type first, fall back to extension only when the browser provides nothing — is generally safer.

The stated goal (normalising video/matroskavideo/x-matroska for .mkv files) could be achieved more narrowly by only applying extension override when the browser-reported type would otherwise fail validation:

export function getMimeType(file: File): string {
  const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
  const extMime = ext ? EXTENSION_TO_MIME[ext] : undefined;
  // Normalise non-standard browser MIME types (e.g. video/matroska -> video/x-matroska)
  if (file.type && extMime && file.type !== extMime) {
    const allSupported = [...SUPPORTED_VIDEO_TYPES, ...SUPPORTED_AUDIO_TYPES, ...SUPPORTED_IMAGE_TYPES];
    if (!allSupported.includes(file.type)) return extMime;
  }
  return file.type || extMime || '';
}

Note: the existing security comment on line 98 already acknowledges client-side MIME spoofing, so this is a best-effort concern, not a blocking one.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/media-library/utils/validation.ts
Line: 65-74

Comment:
**Extension-first detection silently accepts misnamed files**

The updated `getMimeType` now returns the extension-mapped MIME type even when the browser has already reported a valid, different type in `file.type`. This means a file named `not-a-video.mp4` (e.g. a renamed PDF) will pass MIME validation as `video/mp4` because the extension wins over `file.type`. The previous behaviour — trust `file.type` first, fall back to extension only when the browser provides nothing — is generally safer.

The stated goal (normalising `video/matroska``video/x-matroska` for `.mkv` files) could be achieved more narrowly by only applying extension override when the browser-reported type would otherwise fail validation:

```ts
export function getMimeType(file: File): string {
  const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
  const extMime = ext ? EXTENSION_TO_MIME[ext] : undefined;
  // Normalise non-standard browser MIME types (e.g. video/matroska -> video/x-matroska)
  if (file.type && extMime && file.type !== extMime) {
    const allSupported = [...SUPPORTED_VIDEO_TYPES, ...SUPPORTED_AUDIO_TYPES, ...SUPPORTED_IMAGE_TYPES];
    if (!allSupported.includes(file.type)) return extMime;
  }
  return file.type || extMime || '';
}
```

Note: the existing security comment on line 98 already acknowledges client-side MIME spoofing, so this is a best-effort concern, not a blocking one.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/media-library/stores/auto-import-store.ts`:
- Around line 22-23: The audio extension set AUDIO_EXTENSIONS currently omits
".opus" so watched-folder auto-import never detects Opus files; update the
AUDIO_EXTENSIONS constant to include '.opus' (and any other missing audio
extensions) or, preferably, import the canonical list of supported audio file
extensions from the shared validation constant used elsewhere to avoid drift;
apply the same sourcing approach for image/audio extension lists referenced near
IMAGE_EXTENSIONS and the related sets on lines 25-29 so all extension checks use
the single shared source of truth.
- Around line 81-82: The interval starts async work via pollForNewFiles() which
can re-enter if a tick fires while a previous poll is still running, causing
races on pendingFiles/knownFiles and duplicate processing; fix by making
pollForNewFiles non-reentrant: add a local running flag or mutex (e.g.,
isPolling) checked at the top of pollForNewFiles and set/unset around the async
work (or switch from setInterval to a self-scheduling setTimeout that awaits
completion before scheduling the next run), and apply the same change to the
other polling/interval usages in this file (the other timer blocks that
reference timerId/pollForNewFiles/pendingFiles/knownFiles).

In `@src/features/media-library/utils/validation.ts`:
- Around line 66-73: The current logic in validation.ts uses EXTENSION_TO_MIME
via ext as the primary source and can override a valid file.type; change it to
prefer file.type unless it's missing or generic (e.g., empty string or
"application/octet-stream" or other known-generic values), and only then use
EXTENSION_TO_MIME[ext] as a fallback; update the return flow around ext,
EXTENSION_TO_MIME, and file.type so file.type is returned when specific,
otherwise return the mapped extension MIME or empty string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3e575f61-9473-4cb3-9703-fcedba83236d

📥 Commits

Reviewing files that changed from the base of the PR and between c7259e7 and d33d4fc.

📒 Files selected for processing (6)
  • src/features/media-library/components/media-library.tsx
  • src/features/media-library/deps/timeline-actions.ts
  • src/features/media-library/deps/timeline-utils.ts
  • src/features/media-library/stores/auto-import-store.ts
  • src/features/media-library/utils/validation.ts
  • src/features/timeline/contracts/media-library.ts

Comment on lines +22 to +23
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac']);
const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Auto-import extension filter is missing .opus.

Line 22 excludes .opus, so supported Opus files are never detected by watched-folder auto-import even though validation supports them.

💡 Suggested fix
-const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac']);
+const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.opus', '.m4a', '.aac']);
Prefer sourcing supported extensions from a shared validation constant to avoid future drift.

Also applies to: 25-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/media-library/stores/auto-import-store.ts` around lines 22 - 23,
The audio extension set AUDIO_EXTENSIONS currently omits ".opus" so
watched-folder auto-import never detects Opus files; update the AUDIO_EXTENSIONS
constant to include '.opus' (and any other missing audio extensions) or,
preferably, import the canonical list of supported audio file extensions from
the shared validation constant used elsewhere to avoid drift; apply the same
sourcing approach for image/audio extension lists referenced near
IMAGE_EXTENSIONS and the related sets on lines 25-29 so all extension checks use
the single shared source of truth.

Comment on lines +81 to +82
const timerId = setInterval(() => pollForNewFiles(), POLL_INTERVAL_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent poll re-entrancy to avoid overlapping imports.

Line 81 starts an async poll on an interval, but interval ticks can overlap while a previous poll is still running. This can race on pendingFiles/knownFiles and duplicate processing.

💡 Suggested fix
 const POLL_INTERVAL_MS = 3000;
+let pollInProgress = false;
@@
 async function pollForNewFiles(): Promise<void> {
+  if (pollInProgress) return;
+  pollInProgress = true;
+
   const state = useAutoImportStore.getState();
-  if (!state.active || !state.directoryHandle) return;
+  if (!state.active || !state.directoryHandle) {
+    pollInProgress = false;
+    return;
+  }
@@
-  try {
+  try {
@@
   } catch (error) {
     logger.error('Auto-import poll error:', error);
+  } finally {
+    pollInProgress = false;
   }
 }

Also applies to: 126-272

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/media-library/stores/auto-import-store.ts` around lines 81 - 82,
The interval starts async work via pollForNewFiles() which can re-enter if a
tick fires while a previous poll is still running, causing races on
pendingFiles/knownFiles and duplicate processing; fix by making pollForNewFiles
non-reentrant: add a local running flag or mutex (e.g., isPolling) checked at
the top of pollForNewFiles and set/unset around the async work (or switch from
setInterval to a self-scheduling setTimeout that awaits completion before
scheduling the next run), and apply the same change to the other
polling/interval usages in this file (the other timer blocks that reference
timerId/pollForNewFiles/pendingFiles/knownFiles).

Comment on lines +66 to +73
// 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 || '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use extension mapping as fallback only (not primary).

Line 66–Line 73 currently trust extension first for all recognized extensions, which can override a valid non-generic file.type. That conflicts with the intended “fallback when generic” behavior and can misclassify files.

💡 Suggested fix
+const GENERIC_MIME_TYPES = new Set([
+  '',
+  'application/octet-stream',
+  'binary/octet-stream',
+]);
+
 export function getMimeType(file: File): string {
-  // 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).
+  // Use browser MIME when specific; fall back to extension for generic/empty MIME.
   const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
-  if (ext && EXTENSION_TO_MIME[ext]) {
-    return EXTENSION_TO_MIME[ext];
-  }
-  return file.type || '';
+  const extMime = ext ? EXTENSION_TO_MIME[ext] : undefined;
+  const reportedMime = (file.type || '').toLowerCase();
+
+  if (reportedMime && !GENERIC_MIME_TYPES.has(reportedMime)) {
+    return reportedMime;
+  }
+
+  return extMime ?? reportedMime;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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 || '';
const GENERIC_MIME_TYPES = new Set([
'',
'application/octet-stream',
'binary/octet-stream',
]);
export function getMimeType(file: File): string {
// Use browser MIME when specific; fall back to extension for generic/empty MIME.
const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
const extMime = ext ? EXTENSION_TO_MIME[ext] : undefined;
const reportedMime = (file.type || '').toLowerCase();
if (reportedMime && !GENERIC_MIME_TYPES.has(reportedMime)) {
return reportedMime;
}
return extMime ?? reportedMime;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/media-library/utils/validation.ts` around lines 66 - 73, The
current logic in validation.ts uses EXTENSION_TO_MIME via ext as the primary
source and can override a valid file.type; change it to prefer file.type unless
it's missing or generic (e.g., empty string or "application/octet-stream" or
other known-generic values), and only then use EXTENSION_TO_MIME[ext] as a
fallback; update the return flow around ext, EXTENSION_TO_MIME, and file.type so
file.type is returned when specific, otherwise return the mapped extension MIME
or empty string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant