Name: photo-spool Version: 0.4.3 Release: 1%{?dist} Summary: Automatic raw -> lossless DNG photo import from cameras and iPhones # Personal/local tool; MIT is just a permissive default -- change freely. License: MIT URL: https://github.com/lacamar/photo-spool Source0: %{name}-%{version}.tar.gz BuildArch: noarch # App rename (photo-import -> photo-spool), same package otherwise -- lets # `dnf upgrade`/`dnf install` transition a system with the old name # installed (confirmed live on this machine: photo-import has been enabled # and running for a week) straight onto this one instead of leaving both # installed side by side or erroring out. The XDG data dir itself is # migrated separately at runtime (see backend/paths.py's _migrate_data_home) # since that lives under $XDG_DATA_HOME, outside anything rpm tracks. Provides: photo-import = %{version}-%{release} Obsoletes: photo-import < %{version}-%{release} BuildRequires: desktop-file-utils BuildRequires: librsvg2-tools BuildRequires: python3 BuildRequires: systemd-rpm-macros # For %check's unit test suite (tests/) -- mirrors the runtime Requires # below that the tests actually exercise (PySide6 for backend.import_worker # et al., exiftool/ffmpeg for converter.py's real subprocess calls). %check # runs in this build chroot, which only gets BuildRequires installed, not # Requires, so these need to be listed twice. BuildRequires: python3-pyside6 BuildRequires: perl-Image-ExifTool BuildRequires: ffmpeg-free Requires: python3 Requires: python3-pyside6 Requires: python3-dbus Requires: glib2 Requires: udisks2 Requires: dnglab Requires: perl-Image-ExifTool Requires: ffmpeg-free Requires: xdg-utils Requires: hicolor-icon-theme # Camera-as-MTP and iPhone-as-AFC support are advertised, first-class # features (not an optional edge case), so these are hard Requires, not # Recommends -- confirmed live against a real iPhone that a system with # install_weak_deps=False (this machine's own dnf.conf) silently never # installed gvfs-mtp/gvfs-afc at all via a plain Recommends, and that # gvfs-fuse (needed for an MTP/AFC mount to appear as a real POSIX path # to anything, not just be visible to `gio`) was missing from this spec # entirely. SD-card-reader import still works without any of these # (backend/device_watch.py degrades quietly), only MTP/AFC detection # needs them. Requires: gvfs-mtp Requires: gvfs-afc Requires: gvfs-fuse # idevice_id -- an iPhone's main AFC share (the one with DCIM) never # appears as a listed, mountable Volume in `gio mount -li` at all, so # device_watch.py uses this (independent of gvfs's own volume # enumeration) to know when to proactively mount it by its well-known # afc:/// URI. Without it, only the per-app document shares # auto-mount, and the device looks undetected after every replug. Requires: libimobiledevice-utils # Optional: raw-Wayland compositor-blur detection (progressive enhancement, # app works fine without it -- see backend/blur.py). Recommends: python3-pywayland %description A personal, local-only Wayland desktop app that watches for an SD card, a camera (mass storage or MTP), or an iPhone (AFC) being connected, and imports new raw photos: ARW/CR2/CR3/NEF/RAF/RW2/ORF/PEF are converted to lossless-compressed DNG (via the system-packaged dnglab, pulled in as a dependency -- no Adobe DNG Converter needed on Linux); files already in DNG form (e.g. iPhone ProRAW) are copied straight through. Video files (MP4/MOV/M4V/MTS/M2TS/AVI) are filed alongside the stills too, untouched -- just renamed into place, no conversion. Everything is filed into the existing photo library using the same YYYY/YYYY-MM/YYYY-MM-DD/YYYY.MM.DD_Model_NNNNN convention Lightroom was already using (with each file's own original extension). A source strip shows every attached device plus manually pinned folders as clickable icons; opening one shows a thumbnail picker with already-imported shots greyed out (or markable as already-imported without re-processing them) and everything else preselected. Content-hash deduplication means re-scanning a card that still has old photos on it never re-imports anything. Native desktop notifications, XDG-portal-aware light/dark theming, no telemetry, no network access. %prep %autosetup -n %{name}-%{version} %build # Pure Python + QML: nothing to compile. Rasterize the app icon from its # SVG source at the sizes the hicolor theme expects. mkdir -p render for size in 16 32 48 64 128 256 512; do rsvg-convert -w "${size}" -h "${size}" icons/%{name}.svg -o "render/%{name}-${size}.png" done %install install -Dm755 packaging/%{name} %{buildroot}%{_bindir}/%{name} install -d %{buildroot}%{_datadir}/%{name} cp -a main.py backend qml icons %{buildroot}%{_datadir}/%{name}/ desktop-file-install --dir=%{buildroot}%{_datadir}/applications \ --set-key=Exec --set-value=%{name} \ --set-key=Icon --set-value=%{name} \ %{name}.desktop install -Dm644 icons/%{name}.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/%{name}.svg for size in 16 32 48 64 128 256 512; do install -Dm644 "render/%{name}-${size}.png" \ "%{buildroot}%{_datadir}/icons/hicolor/${size}x${size}/apps/%{name}.png" done # A *user* unit (needs the Wayland session/D-Bus user bus), not a system # one -- installed but left disabled by default (no preset ships), so # `systemctl --user enable --now photo-spool.service` is an opt-in step, # not a surprise background app after a routine package upgrade. install -Dm644 packaging/%{name}.service %{buildroot}%{_userunitdir}/%{name}.service %check desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop find %{buildroot}%{_datadir}/%{name} -name '*.py' -print0 | xargs -0 python3 -c ' import ast, sys for path in sys.argv[1:]: with open(path, encoding="utf-8") as fh: ast.parse(fh.read(), path) ' # tests/ self-isolates (every test points XDG_* at a fresh tempdir -- see # tests/testutil.py), so this is safe to run as-is; a failure here fails # the whole build, same as the checks above. python3 -m unittest discover -s tests -v %post %systemd_user_post %{name}.service # Immediately restart for every currently logged-in user who has it # enabled/running, so installing an update actually takes effect right # away instead of waiting for the user to remember to do it by hand. # try-restart is a safe no-op for anyone who never enabled it. # # daemon-reload first, scoped to *this* user's own systemd --user manager # instance -- confirmed live: without it, try-restart can still be using # that manager's in-memory copy of the *previous* unit file (e.g. the # 0.2.31 upgrade that switched Type=simple -> Type=notify), so the # restarted process runs the new code correctly but the manager never # receives its READY=1 (that unit generation predates NotifyAccess even # existing for it) and marks the start "failed (protocol)" -- while the # actual process keeps running fine, just orphaned from systemd's # tracking, invisible to `systemctl status` and never watchdog-protected. loginctl list-users --no-legend 2>/dev/null | while read -r uid user _; do [ -S "/run/user/${uid}/bus" ] || continue XDG_RUNTIME_DIR="/run/user/${uid}" runuser -u "$user" -- \ systemctl --user daemon-reload >/dev/null 2>&1 || : XDG_RUNTIME_DIR="/run/user/${uid}" runuser -u "$user" -- \ systemctl --user try-restart %{name}.service >/dev/null 2>&1 || : done %preun %systemd_user_preun %{name}.service %postun %systemd_user_postun_with_restart %{name}.service %files %{_bindir}/%{name} %{_datadir}/%{name}/ %{_datadir}/applications/%{name}.desktop %{_datadir}/icons/hicolor/*/apps/%{name}.png %{_datadir}/icons/hicolor/scalable/apps/%{name}.svg %{_userunitdir}/%{name}.service %changelog * Wed Sep 23 2026 Photo Spool - 0.4.3-1 - Drop fix-dng-metadata tool from repo * Wed Sep 23 2026 Photo Spool - 0.4.2-1 - Record original raw filename in converted DNGs - Add tools/fix-dng-metadata.py for pre-0.8.0-6 dnglab conversions * Sun Sep 20 2026 Photo Spool - 0.4.1-1 - Restore backend __version__ (0.4.0 failed to start) - Test version matches spec * Sun Sep 20 2026 Photo Spool - 0.4.0-1 - Replace emoji icons with SVG glyphs - Remove in-app notification history - Status bar: percentage, ETA, phase steps - History: day sections, compact empty sessions - Eject moved to source cards - Hide sub-64 MB volumes, show filesystem label and camera model - Cap content width, larger card text - Fix shutdown AttributeError * Mon Aug 31 2026 Photo Spool - 0.3.4-1 - New Settings > DNG conversion option, "Embedded preview size" (small / medium / full size), controlling the preview/thumbnail image embedded in each converted DNG -- not raw image quality, which is unaffected. Maps onto exactly what dnglab's convert command supports: small is thumbnail- only (--dng-preview false), medium is today's default (the camera's own embedded preview), and full size renders the preview from the full raw sensor data via the --full-size-preview flag added by this machine's patched dnglab package (not upstream, see ~/.local/src/arm64-misc/specfiles/dnglab). No separate "large" tier -- dnglab has no continuous preview-resolution control, only that binary preview-source switch plus the thumbnail on/off toggle. * Sun Aug 30 2026 Photo Spool - 0.3.3-1 - Switched from a self-downloaded dnglab binary to the system-packaged dnglab (new Requires: dnglab, built from a separate spec that also adds a --full-size-preview convert flag not upstream). backend/dnglab_setup.py now just does a synchronous shutil.which("dnglab") PATH lookup -- no more network download, no EnsureWorker background thread, no dnglab_path setting. "Retry" in Settings (now "Check again") re-checks PATH instead of re-downloading, for after the package is installed by hand. * Fri Aug 28 2026 Photo Spool - 0.3.2-1 - Switched the local sqlite DB to WAL journal mode with synchronous=NORMAL (backend/db.py). The default rollback-journal mode paid two-plus fsyncs per commit, and ImportWorker._record_file commits once per file during the checking phase -- including every quick-match duplicate hit that never got hashed at all. Re-scanning a mostly-already-imported card was paying hundreds of pure-fsync round trips for files that were never even read, making the checking phase look hung well beyond the actual card-throughput-bound dedup work. * Mon Aug 17 2026 Photo Spool - 0.3.1-1 - Session detail view ("view files from this import") no longer lists files with "duplicate" status. Those rows are every already-imported file the scan happened to re-find on the device, so a card/phone carrying a large existing library re-listed almost its entire import history on every plug-in, which read as if the detail view was showing files from unrelated past imports rather than just this one (backend/app_controller.py's getSessionFiles). Imported and failed files still show; the duplicate count is still visible in the SessionCard summary line above the list. * Mon Aug 17 2026 Photo Spool - 0.3.0-1 - Renamed the app from Photo Import to Photo Spool (package, systemd user service/unit file, desktop file, tray/window/notification text, QML module namespace, single-instance IPC key -- everything). Provides/ Obsoletes the old photo-import package name so `dnf` upgrades a system with it installed straight onto this one instead of leaving both installed side by side. The XDG data dir (session history DB, settings, the self-downloaded dnglab binary) migrates automatically on first run under the new name (backend/paths.py's _migrate_data_home) -- nothing under $XDG_DATA_HOME/photo-import is lost, it's moved in place the first time the app starts post-upgrade. - Note for anyone who already has the old service enabled: the rename means dnf treats this as installing a new package, so %%systemd_user_post's "only auto-enable on a true first install" logic does not carry the old unit's enabled state forward -- re-run `systemctl --user enable --now photo-spool.service` once after upgrading (same one-line opt-in as originally enabling it). - Also throttled the background gvfs/idevice_id device-listing subprocess spawns (device_watch.py) to once per MTP_MOUNT_RETRY_S (12s) instead of every ~2.5s poll tick, cutting idle background CPU/process overhead -- this app spends most of its life sitting in the tray waiting for a card. * Fri Aug 07 2026 Photo Import - 0.2.32-1 - Fixed a packaging bug in %post's own upgrade-restart, confirmed live during the 0.2.31 rollout: try-restart could still be using the running systemd --user manager's in-memory copy of the *previous* unit file (e.g. the 0.2.31 upgrade that switched Type=simple -> Type=notify), so the freshly restarted process ran the new code correctly but its READY=1 was sent to a manager that wasn't expecting it yet -- marked "failed (protocol)" while the process itself kept running fine, just orphaned from systemd's tracking and un-watchdog- protected. %post now runs `systemctl --user daemon-reload` (scoped to each logged-in user) immediately before try-restart. * Fri Aug 07 2026 Photo Import - 0.2.31-1 - Two Wayland-desktop-service polish items, found via a systematic review of the app's OS integration (tray, notifications, theme detection, file-manager reveal, and autostart were all already correct): - The systemd user service is now Type=notify with a 60s watchdog instead of plain Type=simple. The app sends READY=1 once QML startup finishes and pings WATCHDOG=1 on a timer for as long as it runs, so systemd can tell a genuinely wedged GUI thread apart from a normally running one and restart it, instead of a hang sitting there forever as a false "active (running)". - A large card's import (confirmed elsewhere in this codebase as a genuinely multi-minute operation) now holds an idle/suspend inhibitor (org.freedesktop.portal.Inhibit) for its duration, so a laptop suspending mid-transfer can no longer cut an import off partway through. * Fri Aug 07 2026 Photo Import - 0.2.30-1 - Fixed a real bug, confirmed live against the production DB: selecting only a few files to import (via "Import N selected") read their metadata -- and, inside it, inferred any missing camera_model -- over just that narrow selection, instead of the whole card. A video with no embedded Model tag selected without any still alongside it had nothing to borrow a model from, so it got filed with a blank model segment in its filename and a blank camera_model recorded in the ledger. The picker's own preview scan always reads the whole card, so it correctly infers the real model for that same file on every later visit -- which never matched the ledger's blank value, so an already-imported file kept showing up as "new" in the picker indefinitely. Metadata is now always read over the full found file list before any selection narrows it down. * Fri Aug 07 2026 Photo Import - 0.2.29-1 - Codebase audit fixes: - Fixed a real correctness bug: two source files with byte-identical content in the same import batch (confirmed possible -- some cards/cameras keep more than one copy of an identical shot under different names) raced each other for the same staged path, so the second was wrongly reported as "failed" (a confusing "No such file or directory" message) instead of "duplicate". For "mark as already imported" batches this was worse: the second file's ledger INSERT hit the imports.source_hash UNIQUE constraint uncaught, silently killing the whole session (left stuck at status "running" forever, no error ever surfaced). Both now correctly record the second file as a duplicate of the first. - The per-file metadata cache (added in 0.2.26) is now revalidated against the file's current size on every cache hit, not trusted on path alone -- a source mount path (e.g. an SD card reader's mountpoint) can get reused across different physical cards between scans, unlike this app's own destination-library paths. - SessionDetailView now stays live if opened for a session that's still running (e.g. via a "just started" notification), instead of showing a one-shot snapshot frozen at open time. - A few stale/missing code comments cleaned up (CLAUDE.md's description of the passthrough/conversion staging flow, a note on unique_dest_path's check-then-use race, a note on the mtime fallback's timezone choice). * Fri Aug 07 2026 Photo Import - 0.2.28-1 - When a plain-numbered file and its trailing-letter "deferred processing" sibling both exist (e.g. IMG_7731.DNG alongside IMG_7731D.DNG), only the plain one is now imported/shown at all -- the lettered variant is filtered out at the source in scanner.find_importable_files, so this applies transparently to import, the picker, and stats counting alike. A lettered file with no plain sibling present is still kept, since there's nothing to prefer it over. * Fri Aug 07 2026 Photo Import - 0.2.27-1 - Fixed ugly filenames for a real iPhone naming quirk: some assets get a trailing disambiguator letter (e.g. "IMG_7731D.DNG" -- evidence points to Apple's deferred/background photo processing pipeline, confirmed live: a same-capture-second sibling with a very different file size). shot_number()'s trailing-digits regex didn't allow a trailing letter, so a name like that fell all the way back to the *whole* original stem, landing "IMG_" itself in the library filename ("..._IMG_7731D.dng" instead of the intended "..._7731D.dng"). No data was ever lost or overwritten by this -- purely a naming cosmetic issue. * Fri Aug 07 2026 Photo Import - 0.2.26-1 - Scanning a large source (an iPhone's ~700-file camera roll) is now dramatically faster on repeat scans: confirmed live that the exiftool metadata read alone took nearly 21 seconds every single time (picker open, source-card stats refresh, or an import's checking phase), even though the same few hundred files are unchanged scan to scan. Added a persistent per-file metadata cache (new metadata_cache table, keyed by path alone -- this app never modifies a source file once discovered, so its metadata can't actually change later); a warm second scan of the same files confirmed dropping from 20.7s to effectively instant. - Fixed marking/unmarking a file causing the whole thumbnail grid to visibly flash: reassigning the picker's items array to update one file's flag (unavoidable for a plain array, which has no granular change notification) was a full GridView model reset every time -- destroying and recreating every delegate, including every already- decoded thumbnail Image. The picker's item list is now a real ListModel, so mark/unmark update just the one row that changed via setProperty() instead of resetting the whole grid. * Fri Aug 07 2026 Photo Import - 0.2.25-1 - Fixed unmarking a file freezing the UI for several seconds: it was re-deriving the file's metadata via a full recursive scan of the whole source directory (find_importable_files), running synchronously on the GUI thread -- over a slow AFC-mounted iPhone that's multiple seconds of frozen interface, confirmed live (0.12s after the fix, down from multiple seconds). unmarkImported now takes the exact file path the picker already has instead of re-deriving it. - Fixed marking/unmarking a file jumping the thumbnail grid back to the top: reassigning the whole items array (unavoidable -- a plain array has no granular change notification) resets GridView's scroll position like any other model reset. Now saves and restores scroll position around it. - Fixed the picker looking like it forgot everything on every reopen: it cleared to blank and hid the grid immediately on every open, even though the underlying thumbnails were already sitting in the on-disk cache. Reopening the same source now keeps the previous scan's grid (thumbnails and all) visible while a fresh scan runs quietly in the background, with a small "Refreshing…" hint instead of blanking first. - Fixed the source strip's "+ Folder" button sitting flush against the rightmost device card with no gap (ListView's own spacing doesn't reliably apply before a footer item). - Device cards now use distinct icons per device type (💾 SD/storage, 📷 MTP camera, 📱 iPhone, 📁 folder) instead of plain geometric shapes. * Fri Aug 07 2026 Photo Import - 0.2.24-1 - Fixed thumbnails feeling like they were never actually cached: the disk cache added in 0.2.21 stored the *raw* extracted preview/frame as- is, and some embedded RAW previews turn out to be near-full-resolution -- confirmed live, 605 cached files running to 3.6GB total, individual entries up to 14MB, for one device's camera roll. Nothing in this app ever displays a thumbnail larger than ~200px. Decoding a multi-MB JPEG on every redisplay (and blowing Qt's own modest default in-memory pixmap cache budget doing it) was slow enough that a technically-cached thumbnail still looked and felt uncached. Extracted previews/frames are now shrunk to 320px before ever being written to the cache -- the real-world example above dropped from single-digit-MB to ~15KB. The cache directory is now versioned (thumbnails/v2) so old oversized entries are cleanly abandoned rather than continuing to be served. - Added visible, always-on, draggable scrollbars (new ThemedScrollBar component) to the source picker's thumbnail grid and the session- detail file list -- previously QtQuick Controls' default transient, hover-only scrollbar, easy to miss entirely on a long photo list. - Added a way to unmark a file as already imported: clicking a picker tile's "Imported" badge now removes its dedup-ledger entry (never touches an already-placed file, just the bookkeeping record) so it shows up as new again on the next scan -- the missing reverse of the existing "Have it" action, for when a file gets marked by mistake. * Fri Aug 07 2026 Photo Import - 0.2.23-1 - Fixed the real cause of imports getting permanently stuck showing "Filing X/Y" forever, root-caused live with py-spy + the journal: a video with an all-zero placeholder timestamp ("0000:00:00 00:00:00" -- some cameras/video files write this when they never actually recorded one) crashed the whole import session with an uncaught ValueError three frames downstream in converter.library_dest_path ("year must be in 1..9999, not 0"), silently killing the worker thread with no error ever surfaced anywhere -- the session just sat at its last progress update forever. This was a pre-existing bug, not something introduced by recent changes, and had been crashing repeatedly for a while (visible retroactively in the journal). scanner._parse_exif_datetime now validates the parsed value is an actual calendar date/time, not just digit-shaped, and library_dest_path has its own defensive fallback to the file's mtime for the same case, so one file's bad metadata can never take down an entire import again. * Thu Aug 06 2026 Photo Import - 0.2.22-1 - Fixed videos taking dramatically longer to import than photos: every file was being fully read twice over the source connection -- once to hash it for dedup, again to either convert (dnglab) or copy it into place -- and a video is much bigger than a RAW photo, so the same relative inefficiency was far more painful for video. Files are now hashed and staged to a local scratch copy in the same read (scanner.hash_and_stage), and both the conversion and passthrough paths place from that local copy instead of re-reading the original. - Source picker: no longer only ~2 rows visible -- the grid now fills available space instead of a fixed 320px height, shrinking the session history list to a small peek while open. - Fixed thumbnails visibly loading and unloading while scrolling the picker grid: GridView had no cacheBuffer and wasn't reusing delegates, so scrolling a tile off-screen and back destroyed and recreated its Image (discarding the decoded pixmap) every single time. - Submitting a second import for a source that already has one queued or running is now rejected with a toast instead of silently queuing a redundant, wasteful re-scan/re-hash of the same files. - Added a real automated test suite (tests/, stdlib unittest, no new dependency) covering scanner.py, converter.py, settings_store.py, db.py's migrations, and ImportWorker._run_session directly -- wired into `just test` and into this spec's %check, so a failing test now fails the RPM build. Building this surfaced and fixed a real bug in the test infrastructure itself: settings_store.DEFAULTS used to compute library_root's default once at module-import time, so an early version of this suite wrote fake .dng files into the real ~/Pictures before the bug was caught and fixed (library_root's default is now always computed fresh -- see settings_store._default_for). * Thu Aug 06 2026 Photo Import - 0.2.21-1 - Thumbnails are now cached to disk ($XDG_CACHE_HOME/photo-import/ thumbnails), keyed by (path, mtime, size). A source with a large, mostly-already-imported library (an iPhone with hundreds of photos, only a handful new each scan) was re-running exiftool/ffmpeg for every single file on every scan -- confirmed a ~180x speedup on a cache hit in testing. A changed mtime/size naturally misses the cache and just re-extracts, so this needs no explicit invalidation. * Thu Aug 06 2026 Photo Import - 0.2.20-1 - Fixed the iPhone appearing undetected again after a physical unplug/ replug: confirmed live that `gio mount -li` never lists the phone's *main* AFC share (the one with DCIM on it) as a discoverable Volume at all -- only per-app "Files" document shares show up there and auto-mount on their own. device_watch.py now uses `idevice_id` (new Requires: libimobiledevice-utils) to detect physically-connected iPhones independent of gvfs's own incomplete volume listing, and proactively mounts the main share by its well-known afc:/// URI. Verified live: a freshly unmounted device recovers within one ~2s poll cycle, with no manual intervention. * Thu Aug 06 2026 Photo Import - 0.2.19-1 - Fixed a real iPhone import attempt mostly failing (679 of 693 files, "No such file or directory"): the AFC scan walked the phone's entire root, including PhotoData/Mutations/... (iOS's internal Live-Photo-edit bookkeeping), whose entries are inconsistent/semi-virtual over AFC and happened to match VIDEO_EXTENSIONS by name (e.g. ".../Adjustments/ FullSizeRender.mov"). The reported source root is now scoped to DCIM when present -- iPhone-only, not blockdev/MTP, since a Sony camera's video lives in PRIVATE/M4ROOT, a sibling of (not inside) DCIM. - Any session still marked "running" at startup (a crash, an OOM kill, or a service restart landing mid-scan -- all three seen for real while debugging the above) is now reconciled to "failed" instead of leaving a permanently-stuck "running" card in the history forever. * Thu Aug 06 2026 Photo Import - 0.2.18-1 - Fixed iPhone detection not working at all, root-caused live against a real device: - gvfs-mtp/gvfs-afc were never actually installed on this machine -- install_weak_deps=False in dnf.conf means a plain Recommends silently does nothing. Promoted both to hard Requires, since MTP/AFC support is an advertised core feature, not an optional edge case. - Even with those installed, gio mounted the iPhone's AFC share fine at the GVfs API level, but it never appeared as a real file under $XDG_RUNTIME_DIR/gvfs, because gvfsd-fuse was never running -- nothing starts it automatically outside a full GNOME session (a bare niri session, confirmed on this machine, has no such autostart, and gvfs-fuse itself ships only the binary, no unit/autostart entry). device_watch.py now checks /proc/mounts and self-starts gvfsd-fuse if it's not already serving that directory -- same self-managed- dependency approach as dnglab's self-download. Added gvfs-fuse as a new Requires too. - Fixed the device's display name coming back empty/falling back to the raw UDID: the old lookup cross-referenced `gio mount -li`'s free-text name against a dirname reconstructed from a default_location= URI, which breaks for AFC (that URI always ends in "/", so the reconstructed dirname came back empty) and wasn't actually correlatable to the real "afc:host=..." directory name to begin with. Now reads the name directly off the already-discovered path via `gio info`, which is both simpler and correct. - An iPhone's "Files > On My iPhone" per-app document shares (e.g. every installed app's sandboxed Documents folder) were also showing up as their own separate, photo-less "device" card -- filtered out, since only the device's main AFC share ever has a DCIM folder. * Thu Aug 06 2026 Photo Import - 0.2.17-1 - The systemd service now launches quietly: new --start-hidden flag (main.py) skips showing the window on launch (just the tray icon), wired into photo-import.service's ExecStart. A manual launch (desktop icon, terminal) is unaffected -- the window still opens immediately as before. Previously the service opened the window on every start, same as a normal launch. - %post now immediately restarts the service (systemctl --user try-restart, a no-op for anyone who never enabled it) for every currently logged-in user, so a package upgrade actually takes effect right away instead of requiring a manual restart or waiting for next login. * Thu Aug 06 2026 Photo Import - 0.2.16-1 - Added a systemd *user* service (packaging/photo-import.service): starts the app in the background and restarts it (Restart=on-failure, capped at 5 restarts/60s) if it crashes. Tied to graphical-session.target since it needs the Wayland/D-Bus user session, same as niri itself. Installed but not enabled by default (no preset ships, and %post/%preun/ %postun use the standard %systemd_user_* RPM macros) -- a routine package upgrade should never silently start a new background app, so run `systemctl --user enable --now photo-import.service` to opt in. * Thu Aug 06 2026 Photo Import - 0.2.15-1 - Fixed missing camera-model metadata on imported videos: some cameras don't embed a Model tag in video the way they do in stills (confirmed against this machine's own library -- pre-existing videos like "2022.06.03__221935228.mp4" already show the resulting empty-model gap in their filename). If every other file with a known model in the same scan agrees on exactly one (covers Sony's photos/video living in separate directory trees on one card), that model is now borrowed for the video's filename/dedup key and written into the placed video file itself via exiftool. Never infers per-file variable fields (date/time, lens) this way, and backs off entirely on an ambiguous/mixed-camera batch rather than guessing wrong. Verified against real library files. * Thu Aug 06 2026 Photo Import - 0.2.14-1 - "Mark as already imported" is now quiet: it no longer creates a history card, an in-app notification, or a native desktop notification -- it just records the dedup ledger entry in the background (sessions.mark_only, filtered out of the history list/status bar). - Fixed a latent bug this surfaced: imports.session_id was NOT NULL with ON DELETE CASCADE, so clearing a session from history (added in 0.2.10) silently deleted its dedup-ledger rows too, meaning cleared files would look "new" again on the next scan. session_id is now nullable / ON DELETE SET NULL, so the ledger (and the new stats below) survive clearing history. - Source picker: the "Mark unselected as already imported" bulk action is now "Mark selected as already imported", using the same checkbox selection the "Import N selected" button already uses -- select once, then choose which of the two actions applies to that selection. - New library stats page (📊 in the header): lifetime totals (files, storage, photos vs. videos, date range) and a per-camera breakdown, computed from the persistent dedup ledger so it survives history clears. - Fixed a MouseArea stacking bug in the source strip: the whole-tile click-to-open area was declared after (and so on top of) a pinned folder's "✕" remove button, silently swallowing every click on it -- removing a folder has never actually worked until this fix. * Thu Aug 06 2026 Photo Import - 0.2.13-1 - Tray icon behavior split by click: left-click (Trigger) now checks for new importable media (appController.refreshDevices()) and confirms with a native tray balloon instead of toggling the window; opening the window is now exclusively the right-click menu's "Open Photo Import" action, so the two never fight over what a click does. - Video files now get a real thumbnail: they have no embedded-preview tag for exiftool to read (confirmed empty against real iPhone .MOV and camera .mp4 files), so the thumbnail provider now decodes one actual frame via ffmpeg for video paths, used by both the source-picker grid and the session-detail list. New Requires: ffmpeg-free. * Thu Aug 06 2026 Photo Import - 0.2.12-1 - Video files (MP4/MOV/M4V/MTS/M2TS/AVI) are now imported alongside stills: discovered, deduped, and filed into the library using the same YYYY.MM.DD_Model_NNNNN naming convention, but never touched by dnglab -- just renamed and copied through with their original extension, same passthrough path DNG-source files already used. Metadata reads now fall back to CreateDate when a file has no DateTimeOriginal (the normal case for video), so dates/folders come out right for those too. The new DNGBackwardVersion rewrite from 0.2.11 no longer runs against non-DNG destinations. - Fixed two real QML bugs surfaced by an actual run's stderr log: the source picker's selection checkmark warned "Unable to assign [undefined] to bool" for any file whose selection-map entry had been removed (e.g. after "Have it"); and clicking a notification threw "model is not defined" and silently failed to navigate to its session, because markNotificationRead's synchronous model reset was destroying the clicked delegate's context before the next line read it. * Thu Aug 06 2026 Photo Import - 0.2.11-1 - Every DNG landing in the library (both dnglab's own conversions and DNG-passthrough files like iPhone ProRAW) now has its DNGBackwardVersion tag rewritten to 1.4.0.0 in place via exiftool right after it's placed, for compatibility with tools that don't understand newer DNG spec versions -- same fix as the existing standalone ~/.local/bin/dng-version-converter script, now applied automatically at import time instead of as a separate manual pass. Non-fatal if exiftool fails on a given file -- the file stays correctly placed either way. * Thu Aug 06 2026 Photo Import - 0.2.10-1 - Session detail rows: default click now opens the file itself in the desktop's default image viewer (xdg-open on the file); a new small folder button at the right end of each row reveals the file selected in the file manager, via the freedesktop org.freedesktop.FileManager1 ShowItems D-Bus method -- xdg-open can only ever target a folder, never select an item inside it. - Import history: each session card now has a small dismiss control to clear that one entry, plus a header trash-can button to clear all finished sessions at once. Clearing only forgets the app's own history record (DB row, cascades to its per-file rows) -- it never touches the imported photos on disk. A currently-running session can't be cleared. - User-facing copy no longer singles out Sony ARW -- "raw files"/"raw photos" throughout, matching the app's actual multi-format support. * Thu Aug 06 2026 Photo Import - 0.2.9-1 - Subtle motion polish: popups (settings/notifications/session detail) now fade+scale in and out instead of snapping instantly -- QtQuick Controls' Basic style Popup has no default transition at all. The session and notification lists now animate new items in and reflow existing ones instead of jumping. The header bell badge does a small pop when the unread count changes, and header icon buttons give a subtle press-down scale for tactile feedback. - Investigated real Wayland background blur via the ext-background-effect-v1 protocol (niri supports it) -- not implemented: requesting a blur region requires the window's raw wl_surface pointer, which PySide6 6.11 doesn't expose to Python (only the display-wide native interface, not per-window). Would need a compiled C extension; not pursued for now. The existing translucent-background fallback is unchanged. * Thu Aug 06 2026 Photo Import - 0.2.8-1 - Fixed a duplicate-import bug: since 0.2.4 (run-in-background/tray icon), nothing stopped a second launch from starting its own independent instance -- each with its own DeviceWatcher auto-importing from the same attached card, racing to import the same photos and landing on "(2)"/"(3)"/"(4)" duplicate filenames. The app now enforces a single running instance (QLocalServer/QLocalSocket lock in main.py): a second launch just raises the existing window instead of starting duplicate background workers. - Hardened the import pipeline against the underlying race regardless: if two imports ever do land on the same source_hash concurrently, the loser now discards its copy and records a normal "duplicate" instead of crashing the session and leaving an orphan file on disk. * Thu Aug 06 2026 Photo Import - 0.2.7-1 - Thumbnails (session detail view and the source picker grid) now have true squircle (superellipse) corners instead of a plain circular border-radius -- rendered via a new SquircleImage component (Canvas- drawn mask + MultiEffect GPU compositing). - Session detail rows now highlight on hover, matching their new clickability (open-in-file-browser). - Source picker tile's filename caption is now an inset rounded chip instead of a bar flush against the tile edges, so it doesn't clash with the new corner curve. * Wed Aug 05 2026 Photo Import - 0.2.6-1 - App version is now shown at the bottom of the Settings page (backend/__init__.py's __version__ is the single source of truth -- bump it alongside this spec's Version on every release). * Wed Aug 05 2026 Photo Import - 0.2.5-1 - Fixed blank/unclickable thumbnails in the session detail view for duplicate files caught by the fast (no-hash) pre-check: that path was recording an empty dest_path instead of looking up the existing file's actual location, so both the thumbnail and click-to-open (both gated on dest_path) silently did nothing for those rows. - The "importing new photos..." desktop notification now sets the freedesktop "transient" hint, so the desktop's own notification daemon/history panel doesn't retain it either (our own in-app history skip from 0.2.4 only affected this app's history, not the OS one). * Wed Aug 05 2026 Photo Import - 0.2.4-1 - App now runs in the background: closing the main window hides it instead of quitting, and a system tray icon (left-click to show/hide, right-click for Show/Quit) is the only way to actually exit besides Ctrl+C. - Session detail view (the imported-files list) now shows a thumbnail per row and clicking a row opens its destination folder in the default file browser via xdg-open. - Raw-format support widened further: Canon CRM (Cinema RAW Light), Olympus ORI, Panasonic/Leica RAW/RWL, Hasselblad FFF, Sigma X3F, and Apple QuickTake QTK, matching dnglab's full supported-extension list. - "X: importing new photos..." is now a transient desktop notification only (no longer written to the in-app notification history), so plugging in a device repeatedly doesn't clutter the history panel; import-complete and error notifications still persist as before. * Wed Aug 05 2026 Photo Import - 0.2.3-1 - Added a pause/resume control for imports (top status bar): pauses the import queue at the next safe boundary (between files during the scan/hash/place phases, before a conversion batch starts, or before the next queued device starts at all) rather than mid-file. New/auto-detected cards still get queued while paused, just not processed until resumed. - Raw-format support widened from a curated subset to every format dnglab itself supports (Sony SRF/SR2, Canon CRW, Nikon NRW, Minolta MRW, Samsung SRW, Epson ERF, Kodak KDC/DCS/DCR, Hasselblad 3FR, Mamiya MEF, Phase One/Leaf IIQ/MOS, ARRI ARI, alongside the existing ARW/CR2/CR3/ NEF/RAF/RW2/ORF/PEF/DNG) -- a format dnglab can't actually decode just fails that one file with dnglab's own error, never a crash. - Verified DNG-passthrough (already-DNG files, e.g. iPhone ProRAW, copy straight through with no reconversion) and iPhone/MTP-kind import sessions end-to-end against isolated test fixtures. - Source strip cards now show a device/directory kind label (e.g. "SD card / storage", "iPhone", "Camera (MTP)", "Folder") alongside the existing photo counts and capacity bar. * Tue Aug 04 2026 Photo Import - 0.2.2-1 - Fixed the thumbnail preview grid showing filenames/checkboxes but every image tile blank: PySide6's QQuickImageProvider.requestImage binding requires returning a bare QImage (with the out-size written into the passed-in `size` argument), not a (QImage, QSize) tuple as the C++ pointer-out-param signature suggests -- the tuple form failed silently (stderr RuntimeWarning only). - Source strip cards now show live stats instead of just an icon and label: total/not-yet-imported photo counts and a capacity bar (free of total), refreshed automatically on mount, manual refresh, and after an import completes. * Tue Aug 04 2026 Photo Import - 0.2.1-1 - Fixed the source strip device cards being completely unclickable: the sourcesModel's "root" role name shadowed SourceStrip.qml's own `id: root` (ListView delegates auto-expose model roles as bare identifiers), so root.sourceClicked(...) silently threw a QML TypeError on every click instead of opening the preview picker. Renamed the role to "rootPath". * Tue Aug 04 2026 Photo Import - 0.2.0-1 - Source strip: live devices (SD card/camera/iPhone) plus pinned folders as clickable icons; thumbnail picker with per-photo/bulk selection and a "mark as already imported" action that skips conversion entirely. - Multi-format raw support (CR2/CR3/NEF/RAF/RW2/ORF/PEF alongside ARW) and DNG passthrough (copy, no reconversion) for iPhone ProRAW and similar. - iPhone support via AFC (gvfs-afc), alongside existing MTP/mass-storage. - Phased, per-file progress reporting (scanning/checking/converting/placing) plus a persistent top-of-window status bar for the active/queued import -- fixes imports appearing to hang with no feedback during the (genuinely slow, card-reader-bound) hashing phase. - Manual device refresh and on-demand mount-when-clicked for unmounted cards. - RPM build switched to mx-rpm (mock-based), matching this machine's other packages. * Tue Aug 04 2026 Photo Import - 0.1.0-1 - Initial release: UDisks2 + MTP device detection, ARW scanning and content-hash dedup, self-downloaded dnglab lossless DNG conversion, Lightroom-matching library layout, notifications, session history UI.