-
-
Notifications
You must be signed in to change notification settings - Fork 196
feat(media-library): expanded MIME types and auto-import from watched directory #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,5 @@ export { | |
| removeItems, | ||
| updateItem, | ||
| removeItemsFromItemsActions, | ||
| addItem, | ||
| } from './timeline-contract'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,6 @@ | ||
| export { autoMatchOrphanedClips } from './timeline-contract'; | ||
| export { | ||
| buildDroppedMediaTimelineItem, | ||
| type DroppableMediaType, | ||
| getDroppedMediaDurationInFrames, | ||
| } from './timeline-contract'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,272 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { create } from 'zustand'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMediaLibraryStore } from './media-library-store'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useItemsStore } from '@/features/media-library/deps/timeline-stores'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTimelineSettingsStore } from '@/features/media-library/deps/timeline-stores'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useProjectStore } from '@/features/media-library/deps/projects'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildDroppedMediaTimelineItem, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type DroppableMediaType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getDroppedMediaDurationInFrames, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@/features/media-library/deps/timeline-utils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { addItem } from '@/features/media-library/deps/timeline-actions'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { resolveMediaUrl } from '@/features/media-library/utils/media-resolver'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { mediaLibraryService } from '@/features/media-library/services/media-library-service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getMediaType } from '@/features/media-library/utils/validation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createLogger } from '@/shared/logging/logger'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const logger = createLogger('AutoImport'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const POLL_INTERVAL_MS = 3000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isSupportedMediaFile(name: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ext = name.toLowerCase().match(/\.[^.]+$/)?.[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ext) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return VIDEO_EXTENSIONS.has(ext) || AUDIO_EXTENSIONS.has(ext) || IMAGE_EXTENSIONS.has(ext); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Tracks files that were detected but may still be written to (e.g. OBS recordings). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider moving Prompt To Fix With AIThis 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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface AutoImportState { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| active: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directoryHandle: FileSystemDirectoryHandle | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| folderName: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownFiles: Set<string>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timerId: ReturnType<typeof setInterval> | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface AutoImportActions { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable: () => Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disable: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useAutoImportStore = create<AutoImportState & AutoImportActions>()( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (set, get) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| active: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directoryHandle: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| folderName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownFiles: new Set(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timerId: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (get().active) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!('showDirectoryPicker' in window)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useMediaLibraryStore.getState().showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'error', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Directory picker not supported. Please use Google Chrome.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dirHandle = await window.showDirectoryPicker({ mode: 'read' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Snapshot existing files so we only import new ones | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const knownFiles = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for await (const entry of dirHandle.values()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.kind === 'file' && isSupportedMediaFile(entry.name)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownFiles.add(entry.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timerId = setInterval(() => pollForNewFiles(), POLL_INTERVAL_MS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Add a simple boolean guard to serialise poll invocations: Prompt To Fix With AIThis 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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 💡 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| active: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directoryHandle: dirHandle, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| folderName: dirHandle.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownFiles, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timerId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useMediaLibraryStore.getState().showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'success', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Auto-import watching "${dirHandle.name}" for new files`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(`Auto-import enabled for "${dirHandle.name}", ${knownFiles.size} existing files`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error && error.name !== 'AbortError') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error('Failed to enable auto-import:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useMediaLibraryStore.getState().showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'error', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Auto-import failed: ${error.message}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disable: () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { timerId } = get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (timerId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(timerId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingFiles.clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| active: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directoryHandle: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| folderName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownFiles: new Set(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timerId: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info('Auto-import disabled'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function pollForNewFiles(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = useAutoImportStore.getState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state.active || !state.directoryHandle) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { currentProjectId } = useMediaLibraryStore.getState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!currentProjectId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Verify permission is still granted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const permissionStatus = await state.directoryHandle.queryPermission({ mode: 'read' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (permissionStatus !== 'granted') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn('Auto-import: directory permission lost, disabling'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useAutoImportStore.getState().disable(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useMediaLibraryStore.getState().showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'warning', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Auto-import disabled: directory permission lost', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scan directory for new files | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for await (const entry of state.directoryHandle.values()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.kind === 'file' && isSupportedMediaFile(entry.name)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state.knownFiles.has(entry.name) && !pendingFiles.has(entry.name)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // New file detected — add to pending with current size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileHandle = entry as FileSystemFileHandle; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const file = await fileHandle.getFile(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingFiles.set(entry.name, file.size); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(`Auto-import: detected new file "${entry.name}" (${file.size} bytes), waiting for write to finish...`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pendingFiles.size === 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check which pending files are now stable (size > 0 and unchanged since last poll) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stableHandles: FileSystemFileHandle[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+163
to
+196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
Suggested change
Prompt To Fix With AIThis 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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(`Auto-import: importing ${stableHandles.length} stable file(s)`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Import into media library | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const importedMedia = await useMediaLibraryStore.getState().importHandles(stableHandles); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (importedMedia.length === 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Append each imported item to end of timeline on track 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fps = useTimelineSettingsStore.getState().fps; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tracks = useItemsStore.getState().tracks; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const project = useProjectStore.getState().currentProject; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const canvasWidth = project?.metadata.width ?? 1920; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const canvasHeight = project?.metadata.height ?? 1080; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find first visible, unlocked, non-group track | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const targetTrack = tracks.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (t: { visible?: boolean; locked?: boolean; isGroup?: boolean }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.visible !== false && !t.locked && !t.isGroup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!targetTrack) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn('Auto-import: no available track for timeline placement'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const media of importedMedia) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawMediaType = getMediaType(media.mimeType); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rawMediaType === 'unknown') continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mediaType: DroppableMediaType = rawMediaType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const durationInFrames = getDroppedMediaDurationInFrames(media, mediaType, fps); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find end of all items on the target track | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trackItems = useItemsStore.getState().items.filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (i: { trackId: string }) => i.trackId === targetTrack.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const endFrame = trackItems.reduce( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (max: number, item: { from: number; durationInFrames: number }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Math.max(max, item.from + item.durationInFrames), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const blobUrl = await resolveMediaUrl(media.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!blobUrl) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const thumbnailUrl = await mediaLibraryService.getThumbnailBlobUrl(media.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timelineItem = buildDroppedMediaTimelineItem({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| media, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mediaId: media.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mediaType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: media.fileName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timelineFps: fps, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blobUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumbnailUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| canvasWidth, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| canvasHeight, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placement: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackId: targetTrack.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from: endFrame, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| durationInFrames, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addItem(timelineItem); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(`Auto-import: added "${media.fileName}" to timeline at frame ${endFrame}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useMediaLibraryStore.getState().showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'success', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Auto-imported ${importedMedia.length} file(s) to timeline`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error('Auto-import poll error:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Also applies to: 25-29
🤖 Prompt for AI Agents