89 lines
5.5 KiB
Markdown
89 lines
5.5 KiB
Markdown
# AutoIngest: camera photo importer tool
|
|
|
|
**Jeremy Anderson — [dcos.net](https://dcos.net) — info@dcos.net**
|
|
|
|
---
|
|
|
|
## The Problem
|
|
|
|
A friend sells vintage reel-to-reel film on eBay. His workflow is straightforward: photograph film reels with a camera, move the photos to his PC, and attach them to eBay listings in the browser. Every photo needs to be a JPG.
|
|
|
|
His cameras vary, one shoots in .JPG, one shoots in RAW, another . Sometimes he gets HEIC files from an iPhone. The photos land on an SD card, and they need to end up as JPGs in a date-stamped folder on his PC. He is not a technical person. He needs to insert the card and have it work.
|
|
|
|
The original implementation was Python with PySide6, Pillow, rawpy, pillow-heif, cairosvg, and pywin32. That's six packages with native C extensions that need to compile or match pre-built wheels for the target Windows version. On a non-technical person's PC, this is a liability.
|
|
|
|
## The Decision: C# / .NET 8
|
|
|
|
The choice came down to one question: what can we hand the seller as a single file that just works?
|
|
|
|
| Requirement | Python | C# / .NET 8 |
|
|
|-------------|--------|-------------|
|
|
| Deploy as single exe | No (requires interpreter + pip packages) | Yes (`PublishSingleFile`) |
|
|
| Native image format support | 4+ libraries with fragile native builds | One NuGet package (Magick.NET) |
|
|
| Drive detection on Windows | pywin32 (another native dep) | P/Invoke to kernel32 (zero deps) |
|
|
| GUI framework | PySide6 (large native dep) | WinForms (ships with .NET) |
|
|
| Runtime on target PC | Python 3.x + pip + all packages | .NET 8 Desktop Runtime (pre-installed on Win11) |
|
|
| Self-contained option | PyInstaller (fragile, 200+ MB) | `dotnet publish --self-contained` (60-80 MB, reliable) |
|
|
|
|
C# won on every axis that matters for deployment to a non-technical end user.
|
|
|
|
## Architecture
|
|
|
|
The app is organized into three layers (`App`, `Core`, `Engine`) with one NuGet dependency:
|
|
|
|
```
|
|
App/
|
|
Program.cs → Single-instance mutex, entry point
|
|
MainForm.cs → WinForms GUI, dark theme, system tray, log panel
|
|
MainForm.Designer.cs → Toast popup layout (WinForms Designer)
|
|
Core/
|
|
ConfigManager.cs → JSON persistence for app settings
|
|
Engine/
|
|
SDCardMonitor.cs → Background thread polls drives via kernel32
|
|
ImageConverter.cs → Magick.NET wrapper (30+ formats → progressive JPG)
|
|
```
|
|
|
|
### Drive Detection
|
|
|
|
No WMI queries. No COM initialization. Direct P/Invoke to `kernel32.GetDriveType()` and `kernel32.GetVolumeInformationW()`. A 4-second polling interval checks all drive letters, filters out fixed drives (unless they have DCIM folders or camera-related volume labels), and triggers import on new devices.
|
|
|
|
### Import Pipeline
|
|
|
|
1. Discover photos on the drive (DCIM → Pictures → root → 3-level deep scan)
|
|
2. Create `~/Pictures/YYYY-MM-DD/` if it doesn't exist
|
|
3. JPG/JPEG files are **moved** directly via `File.Move` (the card is the source of truth; we don't leave dupes behind)
|
|
4. Everything else goes through Magick.NET: convert color space to sRGB, strip alpha, write as progressive optimized JPG (native resolution preserved — no downscaling), then the original is deleted from the card
|
|
5. Same-day collisions: if a same-named file already exists in today's folder, sizes (then SHA-256) decide — byte-identical files are skipped; different content gets a `_1`, `_2` suffix (capped at 9999, then GUID fallback)
|
|
6. Log every file to the UI log panel
|
|
|
|
### Why System Tray
|
|
|
|
The app is designed to be invisible. The seller inserts an SD card, the app moves and converts silently, and he opens the output folder in the ecommerce listing.
|
|
|
|
## Code Quality
|
|
|
|
The codebase follows a few deliberate conventions:
|
|
|
|
- **Targeted exception handling:** Most `catch` blocks name a specific exception type (`UnauthorizedAccessException`, `IOException`, `MagickCorruptImageErrorException`). A couple of `catch { return false; }` guards remain where any failure should simply mean "not readable."
|
|
- **Named constants over magic numbers:** Intervals (`CHECK_INTERVAL_MS = 4000`), suffix caps (`MAX_COLLISION_SUFFIX = 9999`), colors, and skip-lists are all named fields, not inline literals.
|
|
- **Bounded loops:** The polling thread is a `_running`-flag-controlled `while (_running)` with a fixed sleep, not `while (true)`. Collision suffixes cap at 9999 before falling back to a GUID.
|
|
- **Step-down logic:** Guard clauses and early returns throughout (`ResolveDestPath`, `FindPhotos`, `ShowToast`). The common case executes first.
|
|
- **One responsibility per class:** `SDCardMonitor` monitors drives. `ImageConverter` converts images. `ConfigManager` persists settings. No god objects.
|
|
- **Disposable cleanup:** `MainForm.Dispose` releases the tray bitmap, hide-timer, monitor, and components. `MagickImage` is `using`-scoped. The tray icon bitmap is held for the `NotifyIcon` handle's lifetime.
|
|
- **Comments describe why, not what:** Comments explain design decisions (e.g. why dedupe is scoped to today's folder, why `SetBounds` is used before `Show`).
|
|
|
|
## What's Next
|
|
|
|
Potential improvements for future versions:
|
|
|
|
- MTP/phone detection via Shell.Application COM interop (the Python version had this)
|
|
|
|
## Final Word
|
|
|
|
The Python version was 2900 lines. The C# version is under 1000 lines of source, ships as one file, and has zero runtime dependencies on the target PC (in the self-contained build). The seller gets an exe on his desktop and it works. That's the goal.
|
|
|
|
---
|
|
|
|
**Author:** Jeremy Anderson — [dcos.net](https://dcos.net) — info@dcos.net
|
|
**License:** MS-PL (Microsoft Public License)
|