AutoIngest/BLOG.md

5.5 KiB

AutoIngest: camera photo importer tool

Jeremy Anderson — dcos.netinfo@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.netinfo@dcos.net License: MS-PL (Microsoft Public License)