balotias/statamic-asset-metadata-importer
A Statamic addon for importing metadata into asset fields, helping you bulk-populate and sync asset data from external sources so large media libraries stay organized without manual editing.
1. Install and publish config
composer require <vendor>/statamic-asset-metadata-importer
php artisan vendor:publish --tag=statamic-asset-metadata-importer-config
v1.1.0 is fully backward compatible, so an existing install only needs the composer update unless you want the new adapter options. If you already published the config in a previous version, re-publish with --force (or hand-merge) to get the new adapter_mapping and ffmpeg_path keys.
2. Install at least one extraction backend
The package shells out to (or wraps) external tooling. Pick per file type:
| Adapter | Covers | Requirement |
|---|---|---|
native |
JPG, TIFF | none (built into PHP) |
exiftool |
100+ formats, ~29k tags | exiftool binary (recommended default) |
ffprobe |
MP4, MOV, AVI, MKV, … | FFmpeg installed |
imagick |
200+ formats | PHP imagick extension |
# macOS
brew install exiftool ffmpeg
# Debian/Ubuntu
apt install libimage-exiftool-perl ffmpeg php-imagick
3. Wire up .env
ASSET_METADATA_IMPORTER_FFMPEG_PATH=/usr/bin/ffprobe
Use absolute paths. The PHP-FPM user must be able to execute the binary — this is the single most common reason extraction silently yields nothing.
4. First use case: video metadata on upload
// config/statamic/asset-metadata-importer.php
'adapter_mapping' => [
'native' => ['jpg', 'jpeg', 'tif', 'tiff'],
'exiftool' => ['png', 'webp', 'avif', 'pdf'],
'ffprobe' => ['mp4', 'mov', 'mkv'],
],
Upload an MP4 in the Statamic CP. Duration, codec, dimensions and bitrate land in the mapped fields without any code on your side — the package's upload/reupload listeners do the work.
Where to look first: the new docs/ folder. docs/adapters.md has per-adapter install instructions, docs/configuration.md documents every key, docs/troubleshooting.md covers missing-binary and missing-extension failures. The README is now a 56%-shorter index into those.
Explicit mapping over catch-alls. List extensions per adapter rather than relying on the native fallback. Extension keys are case-insensitive in practice, but normalise to lowercase in config so 'MP4' uploads from macOS clients don't slip through.
Wildcard as a catch-all, with specific overrides above it. '*' is supported in both extensions and adapter_mapping. Order matters — put specific adapters first and the wildcard last so it becomes a fallback:
'adapter_mapping' => [
'ffprobe' => ['mp4', 'mov', 'm4v'],
'native' => ['jpg', 'jpeg', 'tif', 'tiff'],
'exiftool' => ['*'], // anything else, if the binary exists
],
This is a good fit for heterogeneous client uploads (podcast drop folders, DAM imports). It is a bad fit for a 50k-asset library where 80% of files are already mapped — every wildcard hit is a subprocess spawn.
Video fields for the front end. ffprobe output is what you want for <video> markup and schema.org metadata:
{{ if video_duration }}
<meta property="video:duration" content="{{ video_duration }}">
{{ /if }}
<video poster="{{ url }}" controls>
<source src="{{ url }}" type="{{ mime_type }}">
</video>
Store duration as an integer (seconds) or H:i:s string consistently — pick one at fieldset-definition time, because re-backfilling a library later is the annoying part.
Backfilling existing assets. Upload/reupload listeners only fire on new events, so a fresh install on an existing library leaves everything empty. Write a one-off queued job that resolves the adapter for the asset's extension and writes the mapped fields, and wrap each asset in try/catch so one corrupt file doesn't abort the batch:
Asset::all()->each(function ($asset) {
try {
// resolve adapter for $asset->extension() and apply metadata
$asset->save();
} catch (\Throwable $e) {
Log::warning('metadata: ' . $asset->path(), ['e' => $e->getMessage()]);
}
});
Chunk it (->chunk(200)) and dispatch on a queue — exiftool over a large library is CPU-bound and will time out a synchronous request.
Tiered adapter strategy for cost. Native for JPG/TIFF is essentially free; exiftool gives you the deepest tag set; imagick is the pragmatic choice on hosts where you can't install binaries but you can apt install php-imagick. Mixing them (native for the 90% common case, exiftool for the long tail) keeps upload latency down in the CP.
Custom adapters as an extension point. The v1.1.0 refactor into a four-adapter system means the extraction layer is now pluggable — implement the adapter contract for an in-house source (a DAM API, a Cloudflare Images response, a getID3 wrapper) and register it against the extensions it should serve. This is the correct place for S3/remote-asset logic rather than fighting exiftool over stream wrappers.
CI hygiene. Adapter tests that shell out need the binaries present. In pipelines either install exiftool/ffmpeg in the image, or tag those tests @requires / skip when ffprobe_path isn't executable, so a clean runner doesn't fail on environment rather than code.
The native adapter is JPG and TIFF only. It is the default-ish fallback and it will happily "succeed" with nothing useful for a PNG. If PNG/WebP/AVIF metadata matters, you need exiftool or imagick — don't assume native covers "images".
Silent fallback hides misconfiguration. With fallback support, an adapter that can't run (missing binary, missing extension, non-executable path) falls through to the next one. The symptom is missing fields, not an exception. v1.1.0 improved exception handling for missing binaries/extensions, but the practical debugging order is still: run the binary manually as the web user, check storage/logs/laravel.log, then check config.
sudo -u www-data /usr/bin/exiftool -ver
sudo -u www-data /usr/bin/ffprobe -version
php -r 'var_dump(extension_loaded("imagick"));'
Binary paths and quoting. ffmpeg_path is used for FFprobe invocation — point it at the ffprobe binary (or the ffmpeg install prefix, per docs/configuration.md), not at a wrapper script that prints to stdout. Paths with spaces must not be shell-escaped manually in config; the package handles invocation, and double-quoting in .env is the usual cause of "binary not found".
Null-safe config means empty ≠ default. v1.1.0 made config values null-safe. An empty ASSET_METADATA_IMPORTER_FFMPEG_PATH= in .env resolves to null rather than a hardcoded /usr/bin/ffprobe, so the ffprobe adapter just won't be usable. Leave the variable out entirely if you don't want video support rather than setting it blank.
Wildcards hit everything, including things you don't want. '*' in extensions will pull in SVGs, PDFs, fonts, zips and multi-hundred-MB video. Exiftool on a 2 GB file or on a malformed PDF can be slow or hang. On a large library, combine '*' with an explicit deny-list approach in your own listener, or keep the wildcard off in production and use it only for backfill runs.
Imagick is governed by policy.xml. Many distros ship ImageMagick with PDF/PS disabled and tight memory/disk limits. 200+ formats in theory, "no delegate for this format" in practice. Check /etc/ImageMagick-6/policy.xml (or -7) before promising PDF metadata extraction.
Config cache. adapter_mapping is read from cached config in production. After every edit: php artisan config:clear (or config:cache on deploy). A stale cache is a frequent "my new mapping isn't working" report.
Reupload listener behaviour changed. Wildcard support now extends to upload and reupload listeners. If you had a reupload workflow that assumed only explicitly listed extensions were touched, a wildcard will now also fire on re-saves of previously ignored files — worth a test in staging before enabling '*'.
Statamic 6 / PHP 8.5 support is additive, not a green light. Verify your other addons and any custom adapter code against Statamic 6 before bumping the constraint; the metadata extraction path itself is unchanged in behaviour, but the surrounding asset pipeline may not be.
Remote and stream-wrapped assets. ffprobe and exiftool need a local file or a URL they understand. On S3/DO Spaces disks, extract on the temporary local path available during upload rather than on the stored asset URL, and skip extraction entirely for assets written by sync/import jobs unless you control the temp file lifecycle.
Where to extend vs. patch. If you need a new source of metadata, add an adapter (the intended extension point). If you need different field names or conditions, do it in the Statamic fieldset/blueprint — the package maps tags to fields, it isn't the place to shape presentation logic.
How can I help you explore Laravel packages today?