Skip to content

Latest commit

 

History

History
919 lines (727 loc) · 45.7 KB

File metadata and controls

919 lines (727 loc) · 45.7 KB

file-ferry — Spec v0.3.0

Name: file-ferry Repo location: dspury/file-ferry Version: 0.3.0 Status: Released — stable


1. One-liner

A zero-cost CLI for post-production media ops: probe, organize, generate proxies, build DaVinci Resolve projects, and verify backups — all logged to a local SQLite audit trail.

The killer demo: drop a folder of raw media in, run ferry run, and walk away with organized folders, ready-to-edit proxies, a Resolve project file pre-wired with bins and a timeline, and a queryable SQLite audit log proving exactly what happened.


2. Why this exists

Target user: Solo creative operators, small post-production teams, anyone running DaVinci Resolve or other NLEs who needs reliable media infrastructure underneath their edit.

Problem it solves: Most post-production tooling is either (a) expensive SaaS, (b) manual point-and-click workflows that don't scale, or (c) one-off scripts glued together. There is no widely-adopted open-source tool that handles the boring-but-critical media-ops layer (probe → organize → proxy → Resolve project → verify) in one composable, logged, reproducible package.

What it demonstrates: Production-engineering instincts applied to creative media. Codec literacy, FFmpeg fluency, Resolve's Python scripting API, schema thinking, audit discipline.


3. Goals

  1. Zero cost to run. Every dependency is open-source or free. No API keys, no cloud accounts, no paid SaaS.
  2. Single-operator CLI + TUI. Sharp CLI for scripting and automation; interactive Textual TUI (ferry tui) for ad-hoc use. No web UI.
  3. Composable pipeline. Each capability (probe / organize / proxy / resolve / verify) is independent and can be run standalone or chained.
  4. Auditable by default. Every operation writes to a local SQLite log. The log is the system of record for "what happened to my media."
  5. Safe to open-source. No hardcoded paths, hostnames, NAS shares, IPs, or proprietary references anywhere.
  6. Industry-tool native. DaVinci Resolve integration where it adds value, FFmpeg fallback where it doesn't.
  7. Reproducible. Re-running a ferry run on the same input produces the same output and the same log row, modulo timestamps.

4. Non-goals (explicit, with reasons)

Excluded Why
Transcription (Whisper) Out of media-management scope; product is infrastructure, not creative AI
LLM-based content description Same reason
Auto-selects / scoring Creative-AI territory, not infrastructure
Web UI CLI + TUI in v1; browser UI is a v2+ concern
Cloud APIs (any vendor) Zero-cost mandate; local-first mandate
Team collaboration features Single-operator scope
Auto-tagging / ML classification Out of scope for v1; v2 candidate
Scene detection v2 candidate (PySceneDetect is a natural fit, but defer)
Audio loudness / color analysis v2 candidate
Editing features (cuts, transitions) Not the tool's job; Resolve does this

5. v1 Scope — the six capabilities

Shared scanning rule — system artifacts are invisible to every capability. Directory scans (probe, organize, proxy, verify) skip OS/index junk relative to the scan root: any dot-prefixed component (.DS_Store, .Trashes, .Spotlight-V100, AppleDouble ._clip.MP4 sidecars that macOS writes on exFAT/FAT camera cards) plus $RECYCLE.BIN, System Volume Information, LOST.DIR, Thumbs.db, and desktop.ini. Without this, every run against a mounted camera card or backup drive failed or reported noise: ffprobe/ffmpeg choke on AppleDouble sidecars (which carry real video extensions), organize reports them as un-probed, and verify raises false added/modified alarms whenever Finder touches the folder. A scan explicitly rooted inside a hidden directory still works — only components below the root count.

5.1 Probe

Extract structured metadata from any media file (video / audio / image) using ffprobe. Output is a pydantic model capturing:

  • codec, container, width, height, frame_rate, avg_frame_rate, r_frame_rate
  • color_space, color_transfer, color_primaries
  • bit_depth (from bits_per_raw_sample; falls back to parsing pix_fmt for formats that don't report it — e.g., ProRes)
  • is_vfrTrue when r_frame_rate differs from avg_frame_rate by more than 1%; indicates variable-frame-rate source
  • sample_aspect_ratio (SAR) — e.g., "2:1" for anamorphic sources
  • timecode — extracted from format.tags.timecode or video stream disposition.timecode
  • audio_codec, audio_channels, audio_sample_rate, audio_bit_depth
  • duration, file_size, modification time

JSON-serializable, queryable in SQLite.

5.2 Organize

Auto-organize a folder of media into a structured layout based on configurable rules. Default rule: <root>/<source_relpath>/<filename><ext> — the source's subfolder structure is preserved under the destination root (mirrors how DITs think about cards/scenes/takes). Rules live in a config file (ferry.toml) and can be overridden per-project (e.g. {root}/{codec_family}/{resolution_bucket}/{filename}{ext}). Sources are copied by default so raw camera media stays untouched; --move (or mode = "move" in config) relocates instead.

Note: --dry-run is supported — preview the organization plan before touching any files.

Each operation is logged; the manifest is reversible.

5.3 Proxy generation

Generate edit-friendly proxies (default: ProRes 422 Proxy at 1080p, aspect-preserving) from raw camera formats. Output is always a .mov QuickTime file regardless of source container; non-video files are excluded by extension.

Proxy generation is probe-informed. Before generating, the source is probed and the following metadata is used to build the correct ffmpeg command:

  • Timecode — passed via -timecode flag when source carries timecode
  • Color metadata-color_primaries, -color_trc, -colorspace passed through from source
  • SAR / anamorphicsetsar applied after scale to restore correct display aspect ratio
  • Audio codec — PCM bit depth matched to source audio (pcm_s16le for 8–15-bit audio, pcm_s32le for 16+ bit)
  • All audio tracks-map 0:a captures every audio track, not just the first

On same-device organize operations, hardlinks are used instead of full copies to avoid wasted I/O.

Supports MOV, MXF, MP4, and any ffmpeg-readable format. RAW codecs (R3D/BRAW/ARI) are recognized by container but require vendor SDKs for decode — stock ffmpeg cannot decode them.

5.4 DaVinci Resolve project creation

Programmatically create a Resolve project (.drp) from a manifest + a config. Sets project resolution / frame rate / color space; creates a bin structure mirroring the source folder; imports media into the appropriate bins; creates a timeline pre-populated with proxy references. Graceful degradation: if Resolve isn't running/installed, emits a "ready to import" manifest instead and logs a warning.

5.5 Backup verification

Compute fast checksums (xxhash by default; sha256 optional) of all files in a folder; store in SQLite. ferry verify compares current state vs recorded state and reports missing / modified / added files with structured exit codes (0 = clean, 1 = missing, 2 = modified, 3 = added). Designed for shell scripting and cron.

Baseline mutability: Verification does NOT automatically update the stored baseline on mismatch. A mismatch is always reported as an error until explicitly acknowledged. This prevents silent bit-rot: a corrupted file that was missed once does not suppress future detections by overwriting the baseline.

5.6 SQLite audit log (the system of record)

Every operation writes to a local SQLite database. Schema covers: runs, files, probes, proxies, projects, verifications, errors. Queryable via ferry log subcommand (e.g., ferry log --since 1d --missing).

The default location is the platform application-data directory (~/Library/Application Support/ferry/ferry.db on macOS, ~/AppData/Local/ferry/ferry.db on Windows, ~/.local/share/ferry/ferry.db otherwise), resolved by file_ferry.paths.default_db_path so the CLI, the TUI, and the desktop sidecar all address one store. A pre-0.3.1 log at ~/.ferry/ferry.db continues to be used when no application-data database exists. Override with --db or FERRY_DB.


6. Architecture

┌──────────────────────────────────────────────────────────────┐
│                       ferry CLI + TUI                        │
│               (Click for CLI; Textual for TUI)               │
│                                                              │
│    CLI:   ferry probe / organize / proxy / verify ...        │
│    TUI:   ferry tui   (interactive full-screen app)          │
└──────┬──────────┬──────────┬──────────┬───────────┬──────────┘
       │          │          │          │           │
       ▼          ▼          ▼          ▼           ▼
   ┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐  ┌─────────┐
   │ Probe │  │Organiz│  │Proxy  │  │Resolve│  │ Verify  │
   │       │  │  e    │  │ Gen   │  │ Create│  │         │
   └───┬───┘  └───┬───┘  └───┬───┘  └───┬───┘  └────┬────┘
       │          │          │          │            │
       │        ffprobe    ffmpeg   Resolve.py    xxhash
       │          │          │          │  (or      │
       │          │          │          │   ffmpeg  │
       │          │          │          │ fallback) │
       │          │          │          │           │
       └──────────┴────┬─────┴──────────┴───────────┘
                       │
                       ▼
            ┌─────────────────┐
            │  SQLite Audit   │
            │     Log         │
            │ (system of     │
            │  record)        │
            └─────────────────┘

Each box is a Python module with its own tests. The CLI and TUI both compose them.


7. Data model (SQLite schema sketch)

The schema lives in src/file_ferry/log.py (SCHEMA_SQL + LogStore._migrate). The version is recorded in schema_meta and bumped via additive migrations when columns are added — never drop or rename, only ALTER TABLE ADD COLUMN. See SCHEMA_VERSION for the current value.

The code is the source of truth. This block is not hand-maintained against it: tests/test_spec_schema.py executes the SQL below into a scratch database and diffs it against a freshly initialized one, so any drift fails the suite. Note that a few columns (projects.manifest_path, the v7 probes fields) exist only in _migrate, not in SCHEMA_SQL — the sketch reflects the effective schema, which is what a fresh database actually gets. Column order is not compared, because ALTER TABLE ADD COLUMN appends while the sketch groups fields by meaning.

-- Schema version marker. `LogStore.initialize` writes SCHEMA_VERSION here
-- and `_migrate` reads it to decide which additive migrations to apply.
CREATE TABLE schema_meta (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

-- One row per ferry run
CREATE TABLE runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    started_at TEXT NOT NULL,
    finished_at TEXT,
    command TEXT NOT NULL,           -- e.g., "ferry run /path/to/folder"
    config_hash TEXT,                -- hash of the ferry.toml used
    status TEXT NOT NULL,            -- running | success | failed | partial
    error TEXT
);

-- One row per file the system has ever seen
CREATE TABLE files (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    path TEXT NOT NULL UNIQUE,       -- absolute path
    size INTEGER,
    mtime REAL,
    first_seen_run INTEGER REFERENCES runs(id),
    last_seen_run INTEGER REFERENCES runs(id)
);

-- Probe results — populated by `ferry probe`.
-- Every column beyond `id`/`file_id`/`run_id`/`probed_at` is nullable
-- (or default-safe for `is_vfr`) so historical rows survive schema
-- additions unchanged.
CREATE TABLE probes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    file_id INTEGER REFERENCES files(id),
    run_id INTEGER REFERENCES runs(id),
    codec TEXT,                      -- video_codec or audio_codec fallback
    container TEXT,
    width INTEGER,
    height INTEGER,
    frame_rate REAL,
    r_frame_rate REAL,               -- real frame rate (VFR detection, #8)
    is_vfr INTEGER NOT NULL DEFAULT 0,
    color_space TEXT,
    color_transfer TEXT,
    color_primaries TEXT,
    bit_depth INTEGER,
    sample_aspect_ratio TEXT,        -- e.g. "2:1"
    timecode TEXT,                   -- e.g. "01:23:45:12"
    audio_codec TEXT,
    audio_channels INTEGER,
    audio_sample_rate INTEGER,
    audio_bit_depth INTEGER,
    duration REAL,
    modification_time TEXT,
    probed_at TEXT NOT NULL
);

-- Proxy generation records
CREATE TABLE proxies (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    source_file_id INTEGER REFERENCES files(id),
    proxy_path TEXT NOT NULL,
    run_id INTEGER REFERENCES runs(id),
    codec TEXT,
    width INTEGER,
    height INTEGER,
    file_size INTEGER,
    generated_at TEXT NOT NULL
);

-- Resolve project creation records
-- `path` is the requested .drp path (may not exist if Resolve was
-- unavailable). `manifest_path` records what was actually written,
-- so the audit log reflects ground truth.
CREATE TABLE projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    path TEXT NOT NULL,
    manifest_path TEXT,
    run_id INTEGER REFERENCES runs(id),
    resolution TEXT,
    frame_rate TEXT,
    color_space TEXT,
    bin_count INTEGER,
    timeline_count INTEGER,
    resolve_version TEXT,            -- e.g., "20.0"; null if FFmpeg fallback used
    created_at TEXT NOT NULL
);

-- Backup verification records
CREATE TABLE verifications (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    folder TEXT NOT NULL,
    run_id INTEGER REFERENCES runs(id),
    files_checked INTEGER,
    files_missing INTEGER,
    files_modified INTEGER,
    files_added INTEGER,
    checksum_algo TEXT,              -- xxhash | sha256
    verified_at TEXT NOT NULL
);

-- Organize operations — one row per file copied or moved.
-- `link` and `skip` are reserved for future operations; current
-- code only emits `copy` and `move`.
CREATE TABLE organize_ops (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    run_id INTEGER REFERENCES runs(id),
    source_path TEXT NOT NULL,
    destination_path TEXT NOT NULL,
    operation TEXT NOT NULL,          -- copy | move (reserved: link | skip)
    codec_family TEXT,
    resolution_bucket TEXT,
    file_size INTEGER,
    moved_at TEXT NOT NULL
);

-- Verification baseline snapshots — immutable; never mutated on mismatch.
-- `size` and `mtime` are nullable because the snapshot row may exist
-- for a file that has since been removed (so file-level mtime can be
-- reported even when the row is detached from the live folder state).
CREATE TABLE verification_snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    folder TEXT NOT NULL,
    path TEXT NOT NULL,              -- relative path within folder
    checksum TEXT NOT NULL,
    size INTEGER,
    mtime REAL,
    algo TEXT NOT NULL,
    recorded_at TEXT NOT NULL,
    UNIQUE(folder, path)
);

-- Stores one row per verified folder, recording the baseline state.
-- `is_empty=1` means the folder was empty at baseline time (no
-- files). This distinguishes "folder has never had files" from
-- "folder has files" so a file added later is reported as "added",
-- not silently absorbed by an old snapshot row.
CREATE TABLE verification_baselines (
    folder TEXT PRIMARY KEY,
    algo TEXT NOT NULL,
    is_empty INTEGER NOT NULL DEFAULT 0,
    recorded_at TEXT NOT NULL
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_files_path         ON files(path);
CREATE INDEX IF NOT EXISTS idx_probes_file_id     ON probes(file_id);
CREATE INDEX IF NOT EXISTS idx_probes_run_id      ON probes(run_id);
CREATE INDEX IF NOT EXISTS idx_proxies_run_id     ON proxies(run_id);
CREATE INDEX IF NOT EXISTS idx_projects_run_id    ON projects(run_id);
CREATE INDEX IF NOT EXISTS idx_verifications_run_id ON verifications(run_id);
CREATE INDEX IF NOT EXISTS idx_runs_started_at    ON runs(started_at);
CREATE INDEX IF NOT EXISTS idx_organize_ops_run_id  ON organize_ops(run_id);
CREATE INDEX IF NOT EXISTS idx_organize_ops_source ON organize_ops(source_path);
CREATE INDEX IF NOT EXISTS idx_verif_snap_folder  ON verification_snapshots(folder);

8. CLI surface

# Probe a single file
ferry probe clip.mov

# Probe a folder, write to log
ferry probe ./raw/

# Organize a folder using rules in ferry.toml
ferry organize ./raw/ --root ./organized/

# Preview organize without touching files
ferry organize ./raw/ --root ./organized/ --dry-run

# Generate proxies for everything in a folder
ferry proxy ./organized/ --out ./proxies/

# Create a Resolve project from a folder
ferry resolve create ./organized/ --project "Episode-12" --resolution 1080 --fps 24

# Verify a backup
ferry verify ./raw/

# Query the audit log
ferry log --since 1d
ferry log --missing
ferry log --proxies --format json

# Full pipeline
ferry run ./raw/ --organize --proxy --resolve-project --verify

9. Tech stack & dependencies

Component Choice License Cost
Language Python 3.11+ PSF Free
CLI framework Click + Rich (TTY output) BSD / MIT Free
TUI framework Textual (full-screen TUI, ferry tui) MIT Free
Media probe ffprobe (via ffmpeg) LGPL/GPL Free
Transcode / proxy ffmpeg LGPL/GPL Free
Checksum xxhash (python-xxhash) BSD Free
DB SQLite (stdlib) Public domain Free
Models pydantic v2 MIT Free
Terminal output rich MIT Free
Resolve API DaVinci Resolve scripting Free with Resolve Free (Resolve Studio free version supports scripting)
Tests pytest MIT Free
Lint / format ruff MIT Free

Total cost to run: $0.


10. Repo layout

file-ferry/
├── README.md
├── LICENSE                 (MIT)
├── pyproject.toml
├── ferry.toml.example
├── SPEC.md                 (this document)
├── src/file_ferry/
│   ├── __init__.py
│   ├── cli.py              (Click entrypoint)
│   ├── config.py           (ferry.toml loader)
│   ├── probe.py            (ffprobe wrapper)
│   ├── organize.py         (file organizer)
│   ├── proxy.py            (proxy generation)
│   ├── resolve.py          (DaVinci project creation + ffmpeg fallback)
│   ├── verify.py           (checksum verification)
│   ├── log.py              (SQLite audit log)
│   ├── models.py           (pydantic schemas)
│   ├── errors.py           (custom exceptions with exit codes)
│   └── tui.py              (Textual TUI)
├── tests/
│   ├── test_probe.py
│   ├── test_organize.py
│   ├── test_proxy.py
│   ├── test_resolve.py
│   ├── test_verify.py
│   └── test_log.py
├── examples/
│   └── sample-run.md
└── docs/
    └── architecture.md

11. Safety constraints (hard rules)

  1. No hardcoded paths. Everything configurable via CLI args, env vars, or ferry.toml.
  2. No hostnames, IPs, NAS shares, or cloud account refs. Anywhere in the code, docs, or tests.
  3. No proprietary branding or team references. Anywhere.
  4. Public tools only. FFmpeg, ffprobe, pydantic, Click, DaVinci Resolve (free), Python ecosystem.
  5. No paid SaaS fallbacks. Graceful degradation if a piece isn't installed; never "send to API."
  6. No telemetry, no analytics, no remote calls. Truly local.
  7. Test fixtures use synthetic data only. No real media, no real names.

12. First-class terminal interface

ferry with no arguments launches the Textual workstation. Automation and the existing command surface remain available as subcommands; ferry --no-tui explicitly stays in CLI mode and prints command help. ferry tui remains as a compatibility alias. Global --db and --config options are passed through to the TUI.

Screen inventory

Screen Purpose
Home Studio-style launch surface, ffmpeg health, database run totals
Pipelines Browse mounted directories, queue multiple folders, select all five capability steps, and monitor sequential execution. The MEDIA BROWSER pane auto-detects connected external drives (/Volumes/* on macOS minus the system volume, /media/$USER/* and /run/media/$USER/* on Linux, every drive letter except %SYSTEMDRIVE% on Windows) and surfaces them as a clickable list above the directory tree, each entry annotated with free / total disk space.
Audit log Browse up to 500 runs with colored status and incremental command/status search
Settings Edit proxy codec/height, checksum algorithm, ffmpeg path, and Resolve path; save the existing TOML schema

The pipeline executes probe → organize → proxy → resolve → verify. Each queued folder has an independent state (queued, running, done, failed, or cancelled). Capability modules remain the source of truth and are called unchanged. Cancellation therefore occurs safely between capability calls; an in-flight ffmpeg or checksum batch is allowed to finish rather than being killed mid-write. The activity panel reports elapsed time, queue/step progress, and color-coded per-capability result totals. Frame/fps/bitrate telemetry is deferred until the proxy capability exposes a progress callback.

Keyboard contract

Key Action
R, L, S Open Pipelines, Audit Log, or Settings from Home
Arrow keys / Tab / Shift+Tab Move through trees, tables, fields, toggles, and buttons
Enter Expand/select a directory or activate the focused control
A Add the selected browser folder to the pipeline queue
Delete Remove the selected queued folder
Ctrl+R Run the queue sequentially
Ctrl+C Request safe cancellation after the active capability returns
/ Focus audit-log search
Ctrl+S Save settings
Escape Return to the previous screen
Q Quit

The visual system uses a near-black edit-bay background, Resolve-inspired orange for primary actions, purple for stage transitions, cyan for telemetry, and green/yellow/red outcome semantics. Terminal applications cannot select proportional fonts, so hierarchy is expressed with weight, spacing, borders, and color while metadata remains naturally monospace.


13. Versioning rule

file-ferry ships as a beta indefinitely. The version scheme is MAJOR.MINOR.PATCH:

  • MAJOR stays at 0 indefinitely. Never bump to 1.0.0 without D's explicit approval.
  • PATCH bumps (0.1.0 → 0.1.1) are fine for bug fixes — autonomous.
  • MINOR bumps (0.1.0 → 0.2.0) require D's approval — they're feature-signal events.
  • First tagged release: 0.1.0.

14. PyPI publish

The project is file-ferry; the command you type is ferry.

pip install file-ferry
ferry --help
  • PyPI distribution name: file-ferry
  • Python package import name: file_ferry
  • GitHub repo: dspury/file-ferry
  • CLI command: ferry
  • Service entry point: ferry-service

The split is deliberate, and the rule is: the project is file-ferry, the thing you type and see is ferry. Packaging, the import name, the repo and document titles use the project name. The command, its config file (ferry.toml), its data locations (~/.ferry/, Application Support/ferry/), its environment overrides (FERRY_*), the desktop product name and the TUI banner all use the short one, because that is the name a person actually types.

ferry alone was not available on PyPI — it is held by an unrelated, long-abandoned package (OpenCore's Docker tooling, last released 2014-10-19) and PyPI does not reclaim names on request. file-ferry also describes the tool better than a bare ferry does.

Build via python -m build, publish via twine upload (or pyproject.toml-driven trusted publishing on GH Actions).


15. GitHub Actions CI

Workflow at .github/workflows/ci.yml. Matrix:

  • Python 3.10, 3.11, 3.12
  • Each matrix entry: install deps → pytestruff checkruff format --check

Plus a smoke test that runs ferry --help and a small end-to-end probe of a synthetic fixture.


16. Open questions — resolved during build

  1. Click vs Typer.Click. Chosen for its maturity, stable API, and rich output ecosystem (Rich). Typer rejected.
  2. Checksum algo default.xxhash. Implemented as default; sha256 available as opt-in. Speed difference is real and meaningful for large folders.
  3. Default organize rule.codec_family + resolution_bucket. Date rejected — it creates messy paths for multi-day shoots. The two-tier structure is clean and professional.
  4. Sample demo media.FFmpeg testsrc synthetic clips. Five 2-second clips (h264 + ProRes, mixed resolutions) generated with ffmpeg -f lavfi -i testsrc. No real media needed.

17. Future work (v2+ candidates)

  • Scene detection (PySceneDetect)
  • Audio loudness analysis (LUFS / true peak)
  • Color analysis (histogram, dominant color extraction)
  • Resolve round-trip render (export EDL → render → verify)
  • Watch-folder mode (run on file appearance)
  • Web UI (FastAPI + simple HTML)
  • Cloud-storage adapters (S3, GCS) — careful with the "zero cost" mandate
  • Full spanned/multi-file clip model (logical clip abstraction across organize/proxy/resolve)

18. Build order — completed

All items shipped in v0.1.0:

  1. ✅ Scaffold file-ferry/ (pyproject.toml, src layout, tests/, ruff config)
  2. ✅ CI workflow with paths-filter
  3. models.py + log.py (the data layer everything else depends on)
  4. probe.py + tests (simplest capability, validates the pipeline)
  5. organize.py + tests (depends on probe)
  6. proxy.py + tests (depends on probe)
  7. verify.py + tests (independent)
  8. resolve.py + tests (most complex, integrate last)
  9. cli.py (wires it all together)
  10. ✅ README + examples + docs
  11. ✅ GitHub release only; PyPI publish deferred

19. Changes in v0.2.x — v0.3.x

v0.2.2 — Bug fixes and probe enrichment

Status: Released.

Bug fixes

  • Proxy command was probe-ignorant. generate_proxy() never passed request.probe to _ffmpeg_cmd(). Every proxy was generated with safe defaults regardless of source metadata. Timecode, color passthrough, SAR correction, and source-matched PCM bit depth were all unreachable from the public API. Fixed: probe is now passed through.
  • Batch proxy probing always silently failed. ffprobe_path = find_ffmpeg(cfg) was used instead of find_ffprobe(cfg). ffmpeg rejects ffprobe-only arguments and exits with an error, which was silently swallowed. All batch proxy generation was running without probe data. Fixed: correct find_ffprobe now used.
  • Skip-existing proxies logged as failures. When a proxy already existed and --skip-existing was set, the result was recorded as a ProxyFailure. Now recorded as already_existed — a distinct outcome, not a failure.
  • Sidecar files created probe noise. probe_path() recorded every file matching known extensions, including .pek, .pbf, .CTox, and other non-media sidecar formats that ffprobe cannot parse. Now only files that ffprobe can actually parse are recorded.
  • organize --dry-run not exposed on CLI. The underlying organize_path() supported dry_run, but the CLI never exposed it. Fixed: --dry-run is now a CLI flag on the organize command.

New capabilities

  • Bit depth from pix_fmt: MediaProbe.bit_depth now falls back to parsing the pix_fmt field when bits_per_raw_sample is absent (common with ProRes and other intermediate codecs). Maps yuv420p10le → 10-bit, yuv422p8 → 8-bit, etc.
  • VFR detection: MediaProbe now captures r_frame_rate (real frame rate) alongside avg_frame_rate (nominal). is_vfr is True when they diverge by more than 1%. VFR sources (phone recordings, screen captures, action cams) are now flagged in probe output.
  • Timecode extraction: MediaProbe.timecode is extracted from format.tags.timecode or video stream disposition.timecode. Proxies now carry timecode when the source has it.
  • SAR / anamorphic support: MediaProbe.sample_aspect_ratio is captured from the video stream. Proxy generation now applies setsar to restore correct display aspect ratio after scaling, preventing anamorphic sources from producing wrong-shaped proxies.
  • Audio bit depth from probe: Source audio bit depth is extracted from ffprobe stream data and used to select pcm_s16le (8–15-bit audio) or pcm_s32le (16+ bit audio) in the proxy command.
  • --dry-run on organize: CLI preview mode. Files are planned but not moved or copied.

Data model changes

  • MediaProbe: new fields — is_vfr, avg_frame_rate, r_frame_rate, sample_aspect_ratio, timecode, audio_codec, audio_channels, audio_sample_rate, audio_bit_depth
  • ProxyBatchResult: new field already_existed: list[ProxySkip] — proxies skipped because they already existed, distinct from failures
  • New model: ProxySkip — records a source path and proxy path for each skipped file

Resolved issues

Closed in v0.2.2: #7 (SAR), #17 (--dry-run), #19 (proxy drops TC/audio), #25 (editorial fields), #26 (bit depth), #27 (VFR), #28 (skip-existing), #29 (sidecar noise).


v0.2.3 — Code-review remediation + TUI redesign + release hygiene

Status: Released. Open-source release tag.

Bundles three workstreams: a full external code review (verdict: NEEDS CHANGES), a TUI visual-layer redesign, and open-source release hygiene. v0.2.2 landed most code fixes; this release finishes them, fixes two regressions v0.2.2 introduced, corrects invalid shipped config/docs, adds the regression tests the review flagged as missing, gives the Textual workstation a cohesive branded look (see TUI redesign below), and preps the repo for public release (see Release hygiene below).

Bug fixes

  • Silent-video proxy generation hardened. _ffmpeg_cmd() now emits ffmpeg optional-audio mapping (-map 0:v:0 -map 0:a?) instead of a probe-conditional -an/-map 0:a. This both fixes the silent-video failure (screen recordings, action cams) and the inverse bug where a failed ffprobe stripped audio from a source that actually had it.
  • create_resolve_project() crash on default config. v0.2.2's provenance wiring called config.config_hash() instead of cfg.config_hash(), crashing whenever config=None (the default). Fixed; restores the 5 resolve tests.
  • Invalid shipped config examples. ferry.toml.example and the README config block placed proxy_codec/proxy_height/checksum_algo under [organize] (silently dropped) or a [proxy] table (hard ValidationError, because the loader left the table for extra="forbid" to reject). The loader now pops the [proxy] table cleanly; both examples use valid top-level keys.
  • TUI type errors. Three pre-existing Path | None → Path mypy errors in _run_queue() (proxy_source/resolve_source/verify_source) fixed via is None narrowing.

Improvements

  • TUI output isolation (compute_output_tree()): each queued source gets its own subtree under a shared output root, so same-named clips from separate cards no longer collide.
  • TUI dry-run safety: when organize is a dry-run, proxy/resolve/verify are skipped instead of operating on an unpopulated output tree.
  • Audit provenance: all capabilities now pass config_hash to start_run(), so every run is reproducible against its config.

Docs / release hygiene

  • README & SPEC default organize layout corrected to {root}/{source_relpath}/{filename}{ext} (source-structure-preserving).
  • SPEC dropped nonexistent proxy --codec/--height flags.
  • README test count corrected to 292.

Tests

Added 20 regression tests covering the review's false-confidence gaps: real-ffmpeg silent-video proxy generation (@requires_ffmpeg), organize immutability (distinct inode; editing the copy leaves raw untouched), empty-baseline verify (verify empty → add file → reports added), config_hash provenance, and TUI multi-folder output isolation + dry-run-not-poisoning.

TUI redesign (no logic change)

The Textual workstation got a full visual-layer overhaul — zero logic change, all existing widget IDs and pipeline behavior preserved (292 tests still pass; a verify-only smoke run completes done).

  • Branded home dashboard. Refined slant wordmark (replaces the misaligned figlet), strap listing all five capabilities, tagline, and four bordered stat tiles (TOTAL / SUCCEEDED / FAILED / LIVE) with an ffmpeg + db-path line below.
  • Titled panels throughout. Every screen is now grouped into bordered, titled panels: Pipeline → MEDIA BROWSER / QUEUE / CONFIGURE / ACTIVITY with action buttons docked at the bottom; Settings → PROXY / ORGANIZE / PATHS inside a scrollable panel.
  • Logs polish. Bordered AUDIT LOG panel with a live subtitle (N runs, or N shown · M total when filtering); friendlier timestamps (YYYY-MM-DD HH:MM:SS); status rendered as a colored dot + label.
  • Comprehensive CSS rewrite. Round borders, panel backgrounds, design-token spacing, consistent stat-tile / field-label / button styling; the orange primary anchors the brand across all screens.

Release hygiene

Open-source release prep. No code or behavior changes; the package, CLI, TUI, and audit log are unchanged from the sections above. All 292 tests, ruff, and mypy strict still clean.

  • Archive relocated. The frozen SPEC_v0.2.2.md (which still references the old internal monorepo layout, a sibling-products catalog, and an internal author line) moved from the repo root into docs/archive/. It now sits next to the other archived docs and is covered by the .gitignore rule that already excluded that directory. Live SPEC.md and README.md are unchanged and were already clean.
  • .gitignore tightened. Added .venv/, .mypy_cache/, .ruff_cache/, .hypothesis/, *.egg-info/, dist/, build/ so a fresh clone + pip install -e ".[dev]" + git add . can't accidentally commit a virtualenv or tool cache. The existing .pytest_cache/ and __pycache__/ rules were already in place.
  • Attribution pseudonymized. The author's real email was dropped from pyproject.toml; the author field now reads name = "Lunar Park". The LICENSE copyright line was updated to match. The GitHub handle (dspury) and repo URL (github.com/dspury/file-ferry) are unchanged because those are the actual repo location.

v0.2.4 — External-drive reliability + TUI production hardening

Status: Released.

Refinement pass driven by a real failure: running a folder from an external camera card appeared to "not probe or execute" from the TUI.

Bug fixes

  • Activity log was one invisible line. The Log widget's write() appends raw text with no newline and no markup rendering, so every message after the first was concatenated onto a single truncated line showing literal [cyan] tags. Replaced with RichLog (markup, wrapping, real lines). This alone made pipeline failures visible.
  • System artifacts poisoned every capability on removable media. Probe fed AppleDouble ._* sidecars and .DS_Store to ffprobe; proxy sent ._clip.MP4 (video suffix, not video) to ffmpeg; organize reported junk as "no probe data" noise; verify raised false added/modified alarms when Finder touched a folder. A shared is_system_artifact filter (scan-root-relative) now excludes dot-prefixed components, $RECYCLE.BIN, System Volume Information, LOST.DIR, Thumbs.db, desktop.ini from all directory scans.
  • Step checkboxes were invisible. Checkboxes default to 3 rows tall (tall border) but sat in height: 1 containers — the CONFIGURE panel showed no steps at all. Compact one-row styling applied.
  • Resolve option selects were pushed off-screen by a width: 100% Input sharing their row; widths now split the row.
  • Queue State column could be pushed out of view by long paths. State now renders before Folder and paths ellipsize from the left.
  • Bracketed names crashed markup rendering. Paths, filenames, and error text are now markup-escaped everywhere (Addtl [B-cam] etc.).

Reliability / responsiveness

  • Probe gained an optional per-file on_file progress callback; the TUI streams each probed file and lists per-file failure reasons for probe, organize, and proxy (bounded to 5 + count). A probe that yields zero media files now fails the queue item loudly instead of reporting silent success.
  • Drive detection (mount resolution + disk_usage) moved off the UI thread with a 10-second refresh — hot-plugged cards appear without a restart, and a spun-down disk can't freeze the interface.
  • The ffmpeg version check (subprocess, up to 5 s) no longer blocks Home-screen startup; Home stats and the Audit Log refresh on screen resume instead of staying stale for the session.
  • Run-state guards: Remove and re-run are blocked while a queue runs; Esc/Q show a "cancel first" dialog instead of abandoning a half-written copy; the run worker snapshots the queue, honors worker cancellation, and always restores the Run button. Corrupt/foreign audit databases degrade gracefully instead of crashing screens.
  • The MEDIA BROWSER directory tree hides dotfiles and system folders.

v0.3.0 — Rename to file-ferry

The project was renamed from media-mate to file-ferry. No features were removed and no behavior changed; only identity and packaging moved.

Two names, by design (§14): file-ferry is the project — PyPI, the import name, the repo, document titles — and ferry is what you type and see — the command, its config and data locations, its environment overrides, the desktop product name, the TUI banner.

Identity

What Old New
GitHub repo dspury/media-mate dspury/file-ferry
PyPI distribution media-mate file-ferry (see §14)
Python package media_mate file_ferry
CLI command media-mate ferry
Service entry point media-mate-service ferry-service
Config model MediaMateConfig FerryConfig
Preload bridge window.mediaMate window.ferry
Desktop product name media-mate ferry

Paths and environment

What Old New
Project config file ./media-mate.toml ./ferry.toml
Home config ~/.media-mate/config.toml ~/.ferry/config.toml
Legacy audit db ~/.media-mate/media-mate.db ~/.ferry/ferry.db
Desktop app data (macOS) ~/Library/Application Support/media-mate/ ~/Library/Application Support/ferry/
Db backup prefix media-mate-{ISO8601}-pre-{NNN}.db ferry-{ISO8601}-pre-{NNN}.db
Env overrides MEDIA_MATE_CONFIG, MEDIA_MATE_DB, MEDIA_MATE_PROTOCOL_VERSION FERRY_CONFIG, FERRY_DB, FERRY_PROTOCOL_VERSION

There is no automatic migration: the new binary reads the new paths, so a v0.2.x user keeps their old data at the old paths until they move it. The README documents the two mv commands that carry it over. ferry.toml is read from the working directory exactly as media-mate.toml was.

DaVinci Resolve is now explicitly optional. The Resolve API adapter is imported lazily by both the CLI and the TUI, so nothing in the import graph touches DaVinciResolveScript unless a Resolve command actually runs. Resolve has no pip dependency — it is detected at runtime — and the manifest-first fallback (§5) already covers the "Resolve not installed" case. [resolve] and [tui] extras exist to record intent; neither installs anything.

docs/archive/ was removed. The frozen SPEC_v0.2.2.md, architecture.md and sample-run.md described the tool under its old name and were superseded by this document and the README.


20. Open issues — v0.3 candidates

The following issues are acknowledged and targeted for v0.3. Each requires a spec change or design decision before implementation.

#8 — VFR causes audio sync drift in proxies

Severity: Real bug. Recommendation: Add -fps_mode cfr to all proxy generation (forcing constant frame rate from variable-frame-rate sources). This normalizes all proxies to CFR regardless of source — safe because proxies are throwaway edit media. Keep is_vfr in the probe for visibility; the proxy command just normalizes. Versioning impact: None (behavior change is a strict improvement). Status: Worth doing now.

#11 — Same-volume I/O and no parallelism

Severity: Partial. Recommendation: Two parts:

  1. Hardlink on same device — when source and dest are on the same volume, use os.link() instead of shutil.copy2(). Zero I/O overhead, originals stay immutable. Implemented in organize. This is the 80% solution.
  2. Device-aware parallelism — closed as won't-fix for v0.3. Adds significant complexity for marginal gain on a single-operator CLI. Versioning impact: None (hardlink is an optimization, not a behavior change). Status: Hardlink: worth doing now. Parallelism: wont-fix.

#20 — Resolve: empty project

Severity: Spec violation (promised in §5.4). Recommendation: Implement media_pool.ImportMedia() and CreateTimelineFromClips() via the live Resolve API, or demote the capability to "manifest-first" and make the manifest the primary deliverable. The manifest layer is solid; the live-API path produces hollow projects. Versioning impact: Yes — this is the headline Resolve feature. Status: Worth doing now, but requires either Resolve API access for testing or a clear decision to demote to manifest-only.

#21 — Verify silently masks corruption via rolling baseline

Severity: Serious — undermines the core integrity promise. Recommendation: Split "snapshot" (write baseline) from "verify" (compare, never mutate on mismatch). Currently replace_verification_snapshot() is called unconditionally, so a corrupted file's checksum overwrites the good baseline and future runs report clean. With this fix, corruption stays flagged until explicitly acknowledged with --accept-changes. Versioning impact: None — this is fixing a silent correctness bug, not changing advertised behavior. Status: Worth doing now (one-line change in verify.py).

#22 — Organize-by-codec fights AE/DIT mental model

Severity: Real UX problem. Recommendation: Change the default organize template from by_codec to source-structure-preserving (e.g., {root}/{date}/{filename}{ext} or simply mirror source folder under dest). The template system already supports this — it's a default-value change. Codec/resolution remain available as opt-in templates. Versioning impact: Yes — changes the default output layout. Existing configs using explicit templates are unaffected. Status: Worth doing now (default change, minimal code).

#23 — Spanned and multi-file clips split

Severity: Real. Recommendation: Two parts:

  1. Keep groups together — when a multi-file clip is detected (by naming convention), never split the group across organize destinations. Emit a warning so the user knows. This is largely free if #22's source-preserving default is implemented.
  2. Full spanned-clip model — v1.0 candidate. Requires a logical clip abstraction that tracks group membership through organize/proxy/resolve. Retrofitting this into the current per-file model is a significant architecture change. Versioning impact: Partial (warning is none; full model is breaking). Status: Warning: worth doing now. Full model: v1.0.

#24 — R3D/BRAW/ARI fail with stock ffmpeg

Severity: Truth-in-advertising bug. Recommendation: Add a pre-check that detects .r3d, .braw, and .ari extensions and emits a clear error: "R3D decode requires the RED SDK; not supported by stock ffmpeg." Correct the README and SPEC.md §5.3 to say "container recognized; decode requires vendor plugins." Tiny fix, high credibility value. Versioning impact: None (spec correction + error message). Status: Worth doing now.

#30 — Manifest and live-API bin structure mismatch

Severity: Real inconsistency. Recommendation: Fold into #20. When implementing the live-API import path, fix _build_bin_tree to produce nested folder structures that match the manifest's resolve_bin_structure. If recursive bins are not yet supported, deliberately flatten both paths to first-level bins for consistency. Versioning impact: Folded into #20. Status: Worth doing, folded into #20.


21. What you sign off on by approving this doc

All items below were approved at spec time and shipped in v0.1.0:

  • Scope as defined in §5
  • Non-goals as defined in §4
  • Architecture as sketched in §6
  • Data model as sketched in §7
  • Tech stack as defined in §9
  • Safety constraints in §11
  • Versioning rule in §13 (MAJOR=0 until D approves)
  • License: MIT
  • Repo location: dspury/file-ferry
  • Name: file-ferry
  • First tagged version: 0.1.0

All items below were approved and shipped in v0.2.2:

  • Bug fixes in §19
  • New capabilities in §19
  • Data model additions in §19
  • v0.3 candidates as documented in §20

Shipped as approved ✓