Extending
Five filters, all free — they are part of the scanner, not the licensed tier. Between them you can add a detector for a plugin that stores media its own way, overrule any status, and tune how the scan batches.
freshet_unusedmedia_detectors
The detector list, before it runs against one attachment. This is the answer to the blind spot: if a plugin on your site keeps attachment IDs or file URLs in a table of its own, teach the scan to read it.
add_filter( 'freshet_unusedmedia_detectors', function ( array $detectors, $ctx ): array {
$detectors[] = new My_Slider_Detector();
return $detectors;
}, 10, 2 );
$ctx is the attachment context: $ctx->id, $ctx->parentId, and $ctx->basenames — every filename the file can be referenced by (original, -scaled, every registered size, alternate-format copies, encoded spellings). Search for those rather than rebuilding the list.
A detector implements two methods:
interface DetectorInterface {
public function id(): string; // 'my-slider'
public function find( AttachmentContext $ctx ): array; // Reference[]
}
Each Reference it returns carries the detector id, the object type (post, option, theme_mod, term, user, comment), the object id (0 for options), a detail string (the meta key, option name or a short description), the kind of match, and a confidence of confirmed, possible or info. confirmed and possible make the file used; info is shown as evidence and counts for nothing.
Returning references is enough — storing, rendering and the delete-time re-check all follow automatically, because the delete pass runs the same scan.
freshet_unusedmedia_is_used
The final say on a status, after every detector has run.
add_filter( 'freshet_unusedmedia_is_used', function ( bool $used, array $refs, $ctx ): bool {
// Never offer anything in this year's press kit for deletion.
return $used || str_contains( implode( ' ', $ctx->basenames ), 'press-kit-2026' );
}, 10, 3 );
Use it to keep files, by preference. Forcing a file to unused overrides the whole conservative design, including the re-check before deletion — which runs this filter too.
freshet_unusedmedia_upload_grace
How long a freshly uploaded file counts as in use, in seconds. Default DAY_IN_SECONDS (24 hours) — an editor may still be placing it.
add_filter( 'freshet_unusedmedia_upload_grace', fn () => 6 * HOUR_IN_SECONDS );
0 disables the grace entirely. That removes the protection for a file placed in an editor before its post has ever been saved, which is the one case no scanner can see — there is no reference in the database yet. Shorten it if you must; think before you zero it.
freshet_unusedmedia_batch_size
How many attachments one batch works through. Default 10 in the browser scan, 100 on the command line [Pro feature], where there is no request to finish.
add_filter( 'freshet_unusedmedia_batch_size', fn () => 25 );
freshet_unusedmedia_batch_seconds
The browser scan's time budget for one batch, in seconds. Default is half of PHP's max_execution_time, capped at 20; where there is no limit, 20.
add_filter( 'freshet_unusedmedia_batch_seconds', fn () => 10 );
A batch stops when the budget runs out, records the last completed file and returns — so a batch can never hit the execution limit mid-file, fail to advance the cursor, and retry the same IDs forever. Raising this above what the server actually allows re-opens exactly that failure.
Stored data
Everything a scan produces about a file is post meta on that attachment. No tables are created.
| Meta key | |
|---|---|
_freshet_unusedmedia_status |
used or unused |
_freshet_unusedmedia_refs |
{count, refs[]} — the true total, and the first twenty references with detector, object, detail, match and confidence |
_freshet_unusedmedia_scanned_at |
Unix timestamp of the last scan of this file |
Alongside them the plugin keeps a handful of its own options — the scan cursor, a record of the last completed scan, the reclaimed-space ledger and, in the licensed build, the license key and its cached status. Nothing else on the site is written to: no post is edited, no setting of yours is touched.
Reading the meta directly is fine. Writing them is not a supported way to influence a status — the next scan overwrites them, and the delete-time re-check does not read them at all. Use freshet_unusedmedia_is_used for that.